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