293 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			293 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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<void | boolean>
 | 
						|
type MoveHandler = (moveParams?: MouseParams) => Promise<void | boolean>
 | 
						|
type DblClickHandler = (clickParams?: MouseParams) => Promise<void | boolean>
 | 
						|
type DragToHandler = (dragParams: MouseDragToParams) => Promise<void | boolean>
 | 
						|
type DragFromHandler = (
 | 
						|
  dragParams: MouseDragFromParams
 | 
						|
) => Promise<void | boolean>
 | 
						|
 | 
						|
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<SceneSerialised> => {
 | 
						|
    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 }
 | 
						|
      )
 | 
						|
    })
 | 
						|
}
 |