JMLizano's picture
Add playwright tests (#65)
aa9111f
raw
history blame
5.51 kB
// Shared testing utilities.
import { expect, Locator, Page } 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;
constructor(page: Page) {
this.page = page;
}
// Starts with a brand new workspace.
static async empty(page: Page, workspaceName?: string): Promise<Workspace> {
const splash = await Splash.open(page);
return await splash.openNewWorkspace(workspaceName ?? 'test-example');
}
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
return await this.page.locator('select[name="workspace-env"] option').allInnerTexts();
}
async setEnv(env: string) {
await this.page.locator('select[name="workspace-env"]').selectOption(env);
// await this.page.getByRole('combobox', {'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({state: 'visible'});
let nodes: Locator[] = [];
while (nodes.length === 0) {
nodes = await this.getBoxes();
}
}
async addBox(boxName) {
//TODO: Support passing box parameters (id, position, etc.)
const allBoxes = await this.getBoxes();
if (allBoxes) {
// Avoid overlapping with existing nodes
const numNodes = allBoxes.length;
await this.page.mouse.wheel(0, numNodes * 500);
}
// Some x,y offset, otherwise the box handle may fall outside the viewport.
await this.page.locator('.react-flow__pane').click({ position: { x: 20, y: 20 }});
await this.page.getByText(boxName).click();
await this.page.keyboard.press('Escape');
// Workaround to wait for the deselection animation after choosing a box. Otherwise, the next box will not be added.
await new Promise(resolve => setTimeout(resolve, 200));
}
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 new Promise(resolve => setTimeout(resolve, 200));
return catalog
}
async deleteBoxes(boxIds: string[]) {
for (const boxId of boxIds) {
await this.getBoxHandle(boxId).first().click();
await this.page.keyboard.press('Backspace');
}
}
getBox(boxId: string) {
return this.page.locator(`[data-id="${boxId}"]`);
}
getBoxes() {
return this.page.locator('.react-flow__node').all();
}
getBoxHandle(boxId: string) {
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).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)
const targetHandle = this.getBoxHandle(targetId)
await sourceHandle.hover();
await this.page.mouse.down();
await targetHandle.hover();
await this.page.mouse.up();
}
async isErrorFree(): Promise<boolean> {
const boxes = await this.getBoxes();
for (const box of boxes) {
if (await box.locator('.error').isVisible()) {
return false;
}
}
return true;
}
}
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 });
}
async openNewWorkspace(name: string) {
// TODO: Support workspace naming
await this.page.getByRole('link', { name: 'New workspace' }).click();
const ws = new Workspace(this.page);
return ws;
}
async openWorkspace(name: string) {
await this.workspace(name).click();
return new Workspace(this.page);
}
}