File size: 7,162 Bytes
1cbdba3
8fe4e41
1cbdba3
 
 
8fe4e41
1cbdba3
 
8fe4e41
1cbdba3
 
 
58bf1b1
1cbdba3
58bf1b1
1cbdba3
58bf1b1
1cbdba3
 
 
 
 
58bf1b1
1cbdba3
 
 
 
 
0715aa0
1cbdba3
8fe4e41
1cbdba3
58bf1b1
1cbdba3
 
8fe4e41
0715aa0
 
1cbdba3
 
 
 
 
 
 
8fe4e41
1cbdba3
 
 
 
8fe4e41
 
1cbdba3
 
 
 
177f432
1cbdba3
 
45b3519
 
1cbdba3
8fe4e41
5401bd5
 
96431df
 
 
 
177f432
1cbdba3
 
 
9e84b13
 
253206b
 
 
1cbdba3
8fe4e41
177f432
8fe4e41
1cbdba3
8fe4e41
177f432
 
 
 
 
 
 
1cbdba3
 
177f432
8fe4e41
177f432
1cbdba3
 
 
 
 
 
 
 
177f432
1cbdba3
 
45b3519
 
 
 
 
 
1cbdba3
 
 
8fe4e41
 
 
 
 
1cbdba3
badb998
8fe4e41
 
1cbdba3
 
 
8fe4e41
1cbdba3
 
 
 
 
 
8fe4e41
 
 
 
 
1cbdba3
 
 
 
 
45b3519
 
1cbdba3
 
 
 
 
 
177f432
10c9dc3
1cbdba3
58bf1b1
8fe4e41
58bf1b1
 
1cbdba3
 
 
 
 
8fe4e41
1cbdba3
 
8fe4e41
1cbdba3
 
 
 
8fe4e41
1cbdba3
 
 
 
 
 
 
 
 
 
253206b
1cbdba3
 
58bf1b1
8fe4e41
58bf1b1
 
 
8fe4e41
58bf1b1
 
 
 
 
 
8fe4e41
 
 
58bf1b1
8fe4e41
58bf1b1
8fe4e41
1cbdba3
 
 
 
 
58bf1b1
1cbdba3
58bf1b1
 
8fe4e41
58bf1b1
 
 
 
8fe4e41
58bf1b1
 
 
8fe4e41
58bf1b1
 
 
 
8fe4e41
58bf1b1
 
 
 
 
1cbdba3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
// Shared testing utilities.
import { type Locator, type Page, expect } from "@playwright/test";

// Mirrors the "id" filter.
export function toId(x) {
  return x.toLowerCase().replace(/[ !?,./]/g, "-");
}

export const ROOT = "automated-tests";

export class Workspace {
  readonly page: Page;
  name: string;

  constructor(page: Page, workspaceName: string) {
    this.page = page;
    this.name = workspaceName;
  }

  // Starts with a brand new workspace.
  static async empty(page: Page, workspaceName?: string): Promise<Workspace> {
    const splash = await Splash.open(page);
    return await splash.createWorkspace(workspaceName);
  }

  static async open(page: Page, workspaceName: string): Promise<Workspace> {
    const splash = await Splash.open(page);
    const ws = await splash.openWorkspace(workspaceName);
    await ws.waitForNodesToLoad();
    await ws.expectCurrentWorkspaceIs(workspaceName);
    return ws;
  }

  async getEnvs() {
    // Return all available workspace environments
    const envs = this.page.locator('select[name="workspace-env"] option');
    await expect(envs).not.toHaveCount(0);
    return await envs.allInnerTexts();
  }

  async setEnv(env: string) {
    await this.page.locator('select[name="workspace-env"]').selectOption(env);
  }

  async expectCurrentWorkspaceIs(name) {
    await expect(this.page.locator(".ws-name")).toHaveText(name);
  }

  async waitForNodesToLoad() {
    // This method should be used only on non empty workspaces
    await this.page.locator(".react-flow__nodes").waitFor();
    await this.page.locator(".react-flow__node").first().waitFor();
  }

  async addBox(boxName) {
    //TODO: Support passing box parameters (id, position, etc.)
    const allBoxes = await this.getBoxes().all();
    if (allBoxes) {
      // Avoid overlapping with existing nodes
      const numNodes = allBoxes.length || 1;
      await this.page.mouse.wheel(0, numNodes * 400);
    }

    await this.page.locator(".ws-name").click();
    await this.page.keyboard.press("/");
    await this.page
      .locator(".node-search")
      .getByText(boxName, { exact: true })
      .click();
    await expect(this.getBoxes()).toHaveCount(allBoxes.length + 1);
  }

  async getCatalog() {
    await this.page.locator(".ws-name").click();
    await this.page.keyboard.press("/");
    const results = this.page.locator(".node-search .matches .search-result");
    await expect(results.first()).toBeVisible();
    const catalog = await results.allInnerTexts();
    // Dismiss the catalog menu
    await this.page.keyboard.press("Escape");
    await expect(this.page.locator(".node-search")).not.toBeVisible();
    return catalog;
  }

  async selectBox(boxId: string) {
    const box = this.getBox(boxId);
    // Click on the resizer, so we don't click on any parameters by accident.
    await box.locator(".react-flow__resize-control").click();
    await expect(box).toHaveClass(/selected/);
  }

  async deleteBoxes(boxIds: string[]) {
    for (const boxId of boxIds) {
      await this.selectBox(boxId);
      await this.page.keyboard.press("Backspace");
      await expect(this.getBox(boxId)).not.toBeVisible();
    }
  }

  getBox(boxId: string) {
    return this.page.locator(`[data-id="${boxId}"]`);
  }

  getBoxes() {
    return this.page.locator(".react-flow__node");
  }

  getBoxHandle(boxId: string, pos?: string) {
    if (pos) {
      return this.page.locator(
        `[data-id="${boxId}"] [data-handlepos="${pos}"]`,
      );
    }
    return this.page.getByTestId(boxId);
  }

  async moveBox(
    boxId: string,
    offset?: { offsetX: number; offsetY: number },
    targetPosition?: { x: number; y: number },
  ) {
    // Move a box around, it is a best effort operation, the exact target position may not be reached
    const box = await this.getBox(boxId).locator(".title").boundingBox();
    if (!box) {
      return;
    }
    const boxCenterX = box.x + box.width / 2;
    const boxCenterY = box.y + box.height / 2;
    await this.page.mouse.move(boxCenterX, boxCenterY);
    await this.page.mouse.down();
    if (targetPosition) {
      await this.page.mouse.move(targetPosition.x, targetPosition.y);
    } else if (offset) {
      // Without steps the movement is too fast and the box is not dragged. The more steps,
      // the better the movement is captured
      await this.page.mouse.move(
        boxCenterX + offset.offsetX,
        boxCenterY + offset.offsetY,
        { steps: 5 },
      );
    }
    await this.page.mouse.up();
  }

  async connectBoxes(sourceId: string, targetId: string) {
    const sourceHandle = this.getBoxHandle(sourceId, "right");
    const targetHandle = this.getBoxHandle(targetId, "left");
    await sourceHandle.hover();
    await this.page.mouse.down();
    await targetHandle.hover();
    await this.page.mouse.up();
  }

  async expectErrorFree(executionWaitTime?) {
    await expect(this.getBoxes().locator(".error").first()).not.toBeVisible();
  }

  async close() {
    await this.page.locator('a[href="/dir/"]').click();
  }
}

export class Splash {
  page: Page;
  root: Locator;

  constructor(page) {
    this.page = page;
    this.root = page.locator("#splash");
  }

  // Opens the LynxKite directory browser in the root.
  static async open(page: Page): Promise<Splash> {
    await page.goto("/");
    await page.evaluate(() => {
      window.sessionStorage.clear();
      window.localStorage.clear();
    });
    await page.reload();
    const splash = new Splash(page);
    return splash;
  }

  workspace(name: string) {
    return this.page.getByRole("link", { name: name, exact: true });
  }

  getEntry(name: string) {
    return this.page.locator(".entry").filter({ hasText: name }).first();
  }

  async createWorkspace(name?: string) {
    await this.page.getByRole("button", { name: "New workspace" }).click();
    await this.page.locator('input[name="workspaceName"]').click();
    let workspaceName: string;
    if (name) {
      workspaceName = name;
      await this.page.locator('input[name="workspaceName"]').fill(name);
    } else {
      workspaceName = await this.page
        .locator('input[name="workspaceName"]')
        .inputValue();
    }
    await this.page.locator('input[name="workspaceName"]').press("Enter");
    const ws = new Workspace(this.page, workspaceName);
    await ws.setEnv("LynxKite Graph Analytics");
    return ws;
  }

  async openWorkspace(name: string) {
    await this.workspace(name).click();
    return new Workspace(this.page, name);
  }

  async createFolder(folderName?: string) {
    await this.page.getByRole("button", { name: "New folder" }).click();
    await this.page.locator('input[name="folderName"]').click();
    if (folderName) {
      await this.page.locator('input[name="folderName"]').fill(folderName);
    }
    await this.page.locator('input[name="folderName"]').press("Enter");
  }

  async deleteEntry(entryName: string) {
    await this.getEntry(entryName).locator("button").click();
    await this.page.reload();
  }

  currentFolder() {
    return this.page.locator(".current-folder");
  }

  async goHome() {
    await this.page.locator('a[href="/dir/"]').click();
  }
}