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