File size: 6,883 Bytes
aa9111f
7ed5764
aa9111f
 
 
7ed5764
aa9111f
 
7ed5764
aa9111f
 
 
938d45b
aa9111f
938d45b
aa9111f
938d45b
aa9111f
 
 
230cf12
aa9111f
938d45b
aa9111f
 
 
 
 
dff86b8
aa9111f
7ed5764
aa9111f
938d45b
aa9111f
 
7ed5764
dff86b8
 
aa9111f
 
 
 
 
 
 
7ed5764
aa9111f
 
 
 
7ed5764
 
aa9111f
 
 
 
1eb900a
aa9111f
 
d9aeaae
 
aa9111f
7ed5764
83c09d6
 
1270bff
1eb900a
aa9111f
 
 
5282211
 
284a2da
 
 
aa9111f
7ed5764
1eb900a
7ed5764
aa9111f
7ed5764
1eb900a
 
 
 
 
 
 
aa9111f
 
1eb900a
7ed5764
1eb900a
aa9111f
 
 
 
 
 
 
 
1eb900a
aa9111f
 
d9aeaae
 
1270bff
d9aeaae
aa9111f
 
 
7ed5764
 
 
 
 
aa9111f
22a68fe
7ed5764
 
aa9111f
 
 
7ed5764
aa9111f
 
 
 
 
 
1270bff
 
 
aa9111f
 
 
 
 
d9aeaae
 
aa9111f
 
 
 
 
 
0fcd182
 
 
 
 
 
1eb900a
0fcd182
aa9111f
938d45b
7ed5764
90b31da
938d45b
aa9111f
 
 
 
 
7ed5764
aa9111f
 
7ed5764
aa9111f
 
 
 
7ed5764
aa9111f
 
 
 
 
 
 
 
 
 
284a2da
aa9111f
 
938d45b
7ed5764
938d45b
 
230cf12
7ed5764
230cf12
 
 
 
7ed5764
aa9111f
 
 
 
 
938d45b
aa9111f
938d45b
230cf12
7ed5764
230cf12
 
 
938d45b
 
 
7ed5764
938d45b
 
 
 
7ed5764
938d45b
 
 
90b31da
938d45b
aa9111f
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
// 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 execute() {
    const request = this.page.waitForResponse(/api[/]execute_workspace/);
    await this.page.keyboard.press("r");
    await request;
  }

  async expectErrorFree(executionWaitTime?) {
    await expect(this.getBoxes().locator("text=⚠️").first()).not.toBeVisible();
  }

  async close() {
    await this.page.getByRole("link", { name: "close" }).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();
    const nameBox = this.page.locator('input[name="entryName"]');
    await nameBox.fill(name);
    await nameBox.press("Enter");
    const ws = new Workspace(this.page, name);
    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();
    const nameBox = this.page.locator('input[name="entryName"]');
    await nameBox.fill(folderName);
    await nameBox.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.getByRole("link", { name: "home" }).click();
  }
}