Spaces:
Running
Running
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();
}
}
|