Spaces:
Running
Running
File size: 7,286 Bytes
18d76df d460634 18d76df d460634 18d76df d460634 18d76df dc0212e 18d76df dc0212e 18d76df dc0212e 18d76df a3bf6a6 18d76df dc0212e 18d76df da3ce8e 18d76df d460634 18d76df dc0212e 18d76df d460634 da3ce8e 18d76df d460634 18d76df d460634 18d76df 7194a89 c83f1c9 f94e9a7 a112474 c83f1c9 18d76df dcd48d1 c82d8dc 18d76df d460634 c83f1c9 d460634 18d76df d460634 c83f1c9 18d76df c83f1c9 d460634 c83f1c9 18d76df c83f1c9 18d76df a2c3c92 19adf28 18d76df d460634 18d76df 7ec3f60 d460634 18d76df d460634 18d76df a112474 18d76df 486cb5c bf8f4f1 b546ff0 18d76df 486cb5c 18d76df 19adf28 b546ff0 486cb5c 19adf28 18d76df a697ae8 c83f1c9 a697ae8 18d76df dc0212e d460634 0234ec8 dc0212e 18d76df d460634 18d76df d460634 18d76df d460634 18d76df c82d8dc 18d76df dc0212e d460634 dc0212e a3bf6a6 d460634 a3bf6a6 d460634 18d76df dc0212e 18d76df dc0212e a3bf6a6 d460634 a3bf6a6 dc0212e d460634 dc0212e d460634 dc0212e 0234ec8 dc0212e 18d76df |
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 |
// 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.
const allBoxes = await this.getBoxes().all();
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) {
return this.page.locator(`.connectable[data-nodeid="${boxId}"][data-handlepos="${pos}"]`);
}
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 tryToConnectBoxes(sourceId: string, targetId: string) {
const sourceHandle = this.getBoxHandle(sourceId, "right");
const targetHandle = this.getBoxHandle(targetId, "left");
await expect(sourceHandle).toBeVisible();
await expect(targetHandle).toBeVisible();
await sourceHandle.hover();
await this.page.mouse.down();
await expect(this.page.locator(".react-flow__connectionline")).toBeAttached({ timeout: 1000 });
await targetHandle.hover();
await this.page.mouse.up();
await expect(
this.page.locator(`.react-flow__edge[aria-label="Edge from ${sourceId} to ${targetId}"]`),
).toBeAttached({ timeout: 1000 });
}
async connectBoxes(sourceId: string, targetId: string) {
// The method above is unreliable. I gave up after a lot of debugging and added these retries.
while (true) {
try {
await this.tryToConnectBoxes(sourceId, targetId);
return;
} catch (e) {}
}
}
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();
}
}
|