JMLizano's picture
Single import box for multiple file formats (#80)
10c9dc3 unverified
raw
history blame
7.4 kB
// 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 new Promise((resolve) => setTimeout(resolve, 200));
}
// Some x,y offset, otherwise the box handle may fall outside the viewport.
await this.page.locator(".ws-name").click();
await this.page.keyboard.press("/");
await this.page.locator(".node-search").getByText(boxName).click();
await expect(this.getBoxes()).toHaveCount(allBoxes.length + 1);
}
async getCatalog() {
await this.page.locator(".react-flow__pane").click();
const catalog = await this.page
.locator(".node-search .matches .search-result")
.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?) {
// TODO: Workaround, to account for workspace execution. Once
// we have a load indicator we can use that instead.
await new Promise((resolve) =>
setTimeout(resolve, executionWaitTime ? executionWaitTime : 500),
);
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 });
}
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();
}
}