import type { Page, Locator } from '@playwright/test' import { expect } from '@playwright/test' import { isArray, uuidv4 } from 'lib/utils' import { closeDebugPanel, doAndWaitForImageDiff, getPixelRGBs, openAndClearDebugPanel, sendCustomCmd, } from '../test-utils' type MouseParams = { pixelDiff?: number shouldDbClick?: boolean delay?: number } type MouseDragToParams = MouseParams & { fromPoint: { x: number; y: number } } type MouseDragFromParams = MouseParams & { toPoint: { x: number; y: number } } type SceneSerialised = { camera: { position: [number, number, number] target: [number, number, number] } } type ClickHandler = (clickParams?: MouseParams) => Promise type MoveHandler = (moveParams?: MouseParams) => Promise type DblClickHandler = (clickParams?: MouseParams) => Promise type DragToHandler = (dragParams: MouseDragToParams) => Promise type DragFromHandler = ( dragParams: MouseDragFromParams ) => Promise export class SceneFixture { public page: Page public streamWrapper!: Locator public loadingIndicator!: Locator get exeIndicator() { return this.page.getByTestId('model-state-indicator-execution-done') } constructor(page: Page) { this.page = page this.reConstruct(page) } private _serialiseScene = async (): Promise => { const camera = await this.getCameraInfo() return { camera, } } expectState = async (expected: SceneSerialised) => { return expect .poll(async () => await this._serialiseScene(), { intervals: [1_000, 2_000, 10_000], timeout: 60000, }) .toEqual(expected) } reConstruct = (page: Page) => { this.page = page this.streamWrapper = page.getByTestId('stream') this.loadingIndicator = this.streamWrapper.getByTestId('loading') } makeMouseHelpers = ( x: number, y: number, { steps }: { steps: number } = { steps: 20 } ): [ClickHandler, MoveHandler, DblClickHandler] => [ (clickParams?: MouseParams) => { if (clickParams?.pixelDiff) { return doAndWaitForImageDiff( this.page, () => clickParams?.shouldDbClick ? this.page.mouse.dblclick(x, y, { delay: clickParams?.delay || 0, }) : this.page.mouse.click(x, y, { delay: clickParams?.delay || 0, }), clickParams.pixelDiff ) } return clickParams?.shouldDbClick ? this.page.mouse.dblclick(x, y, { delay: clickParams?.delay || 0 }) : this.page.mouse.click(x, y, { delay: clickParams?.delay || 0 }) }, (moveParams?: MouseParams) => { if (moveParams?.pixelDiff) { return doAndWaitForImageDiff( this.page, () => this.page.mouse.move(x, y, { steps }), moveParams.pixelDiff ) } return this.page.mouse.move(x, y, { steps }) }, (clickParams?: MouseParams) => { if (clickParams?.pixelDiff) { return doAndWaitForImageDiff( this.page, () => this.page.mouse.dblclick(x, y), clickParams.pixelDiff ) } return this.page.mouse.dblclick(x, y) }, ] as const makeDragHelpers = ( x: number, y: number, { steps }: { steps: number } = { steps: 20 } ): [DragToHandler, DragFromHandler] => [ (dragToParams: MouseDragToParams) => { if (dragToParams?.pixelDiff) { return doAndWaitForImageDiff( this.page, () => this.page.dragAndDrop('#stream', '#stream', { sourcePosition: dragToParams.fromPoint, targetPosition: { x, y }, }), dragToParams.pixelDiff ) } return this.page.dragAndDrop('#stream', '#stream', { sourcePosition: dragToParams.fromPoint, targetPosition: { x, y }, }) }, (dragFromParams: MouseDragFromParams) => { if (dragFromParams?.pixelDiff) { return doAndWaitForImageDiff( this.page, () => this.page.dragAndDrop('#stream', '#stream', { sourcePosition: { x, y }, targetPosition: dragFromParams.toPoint, }), dragFromParams.pixelDiff ) } return this.page.dragAndDrop('#stream', '#stream', { sourcePosition: { x, y }, targetPosition: dragFromParams.toPoint, }) }, ] as const /** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene. * * Expects the viewPort to be 1000x500 */ clickNoWhere = () => this.page.mouse.click(998, 60) /** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene. * * Expects the viewPort to be 1000x500 */ moveNoWhere = (steps?: number) => this.page.mouse.move(998, 60, { steps }) moveCameraTo = async ( pos: { x: number; y: number; z: number }, target: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 } ) => { await openAndClearDebugPanel(this.page) await doAndWaitForImageDiff( this.page, () => sendCustomCmd(this.page, { type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_look_at', vantage: pos, center: target, up: { x: 0, y: 0, z: 1 }, }, }), 300 ) await closeDebugPanel(this.page) } /** Forces a refresh of the camera position and target displayed * in the debug panel and then returns the values of the fields */ async getCameraInfo() { await openAndClearDebugPanel(this.page) await sendCustomCmd(this.page, { type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_get_settings', }, }) await this.page .locator(`[data-receive-command-type="default_camera_get_settings"]`) .first() .waitFor() const position = await Promise.all([ this.page.getByTestId('cam-x-position').inputValue().then(Number), this.page.getByTestId('cam-y-position').inputValue().then(Number), this.page.getByTestId('cam-z-position').inputValue().then(Number), ]) const target = await Promise.all([ this.page.getByTestId('cam-x-target').inputValue().then(Number), this.page.getByTestId('cam-y-target').inputValue().then(Number), this.page.getByTestId('cam-z-target').inputValue().then(Number), ]) await closeDebugPanel(this.page) return { position, target, } } waitForExecutionDone = async () => { await expect(this.exeIndicator).toBeVisible({ timeout: 30000 }) } expectPixelColor = async ( colour: [number, number, number] | [number, number, number][], coords: { x: number; y: number }, diff: number ) => { await expectPixelColor(this.page, colour, coords, diff) } get gizmo() { return this.page.locator('[aria-label*=gizmo]') } async clickGizmoMenuItem(name: string) { await this.gizmo.hover() await this.gizmo.click({ button: 'right' }) const buttonToTest = this.page.getByRole('button', { name: name, }) await expect(buttonToTest).toBeVisible() await buttonToTest.click() } } function isColourArray( colour: [number, number, number] | [number, number, number][] ): colour is [number, number, number][] { return isArray(colour[0]) } export async function expectPixelColor( page: Page, colour: [number, number, number] | [number, number, number][], coords: { x: number; y: number }, diff: number ) { let finalValue = colour await expect .poll( async () => { const pixel = (await getPixelRGBs(page)(coords, 1))[0] if (!pixel) return null finalValue = pixel if (!isColourArray(colour)) { return pixel.every( (channel, index) => Math.abs(channel - colour[index]) < diff ) } return colour.some((c) => c.every((channel, index) => Math.abs(pixel[index] - channel) < diff) ) }, { timeout: 10_000 } ) .toBeTruthy() .catch((cause) => { throw new Error( `ExpectPixelColor: expecting ${colour} got ${finalValue}`, { cause } ) }) }