588 lines
19 KiB
TypeScript
588 lines
19 KiB
TypeScript
import type { EngineCommand } from '@src/lang/std/artifactGraph'
|
|
import { uuidv4 } from '@src/lib/utils'
|
|
|
|
import { getUtils } from '@e2e/playwright/test-utils'
|
|
import { expect, test } from '@e2e/playwright/zoo-test'
|
|
import type { Page } from '@playwright/test'
|
|
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
|
|
|
|
test.describe('Testing Camera Movement', () => {
|
|
/**
|
|
* hack that we're implemented our own retry instead of using retries built into playwright.
|
|
* however each of these camera drags can be flaky, because of udp
|
|
* and so putting them together means only one needs to fail to make this test extra flaky.
|
|
* this way we can retry within the test
|
|
* We could break them out into separate tests, but the longest past of the test is waiting
|
|
* for the stream to start, so it can be good to bundle related things together.
|
|
*/
|
|
const bakeInRetries = async ({
|
|
mouseActions,
|
|
afterPosition,
|
|
beforePosition,
|
|
retryCount = 0,
|
|
page,
|
|
scene,
|
|
}: {
|
|
mouseActions: () => Promise<void>
|
|
beforePosition: [number, number, number]
|
|
afterPosition: [number, number, number]
|
|
retryCount?: number
|
|
page: Page
|
|
scene: SceneFixture
|
|
}) => {
|
|
const acceptableCamError = 5
|
|
const u = await getUtils(page)
|
|
|
|
await test.step('Set up initial camera position', async () =>
|
|
await scene.moveCameraTo({
|
|
x: beforePosition[0],
|
|
y: beforePosition[1],
|
|
z: beforePosition[2],
|
|
}))
|
|
|
|
await test.step('Do actions and watch for changes', async () =>
|
|
u.doAndWaitForImageDiff(async () => {
|
|
await mouseActions()
|
|
|
|
await u.openAndClearDebugPanel()
|
|
await u.closeDebugPanel()
|
|
await page.waitForTimeout(100)
|
|
}, 300))
|
|
|
|
await u.openAndClearDebugPanel()
|
|
await expect(page.getByTestId('cam-x-position')).toBeAttached()
|
|
|
|
const vals = await Promise.all([
|
|
page.getByTestId('cam-x-position').inputValue(),
|
|
page.getByTestId('cam-y-position').inputValue(),
|
|
page.getByTestId('cam-z-position').inputValue(),
|
|
])
|
|
const errors = vals.map((v, i) => Math.abs(Number(v) - afterPosition[i]))
|
|
let shouldRetry = false
|
|
|
|
if (errors.some((e) => e > acceptableCamError)) {
|
|
if (retryCount > 2) {
|
|
console.log('xVal', vals[0], 'xError', errors[0])
|
|
console.log('yVal', vals[1], 'yError', errors[1])
|
|
console.log('zVal', vals[2], 'zError', errors[2])
|
|
|
|
throw new Error('Camera position not as expected', {
|
|
cause: {
|
|
vals,
|
|
errors,
|
|
},
|
|
})
|
|
}
|
|
shouldRetry = true
|
|
}
|
|
if (shouldRetry) {
|
|
await bakeInRetries({
|
|
mouseActions,
|
|
afterPosition: afterPosition,
|
|
beforePosition: beforePosition,
|
|
retryCount: retryCount + 1,
|
|
page,
|
|
scene,
|
|
})
|
|
}
|
|
}
|
|
|
|
test(
|
|
'Can pan and zoom camera reliably',
|
|
{
|
|
tag: '@web',
|
|
},
|
|
async ({ page, homePage, scene, cmdBar }) => {
|
|
const u = await getUtils(page)
|
|
const camInitialPosition: [number, number, number] = [0, 85, 85]
|
|
|
|
await homePage.goToModelingScene()
|
|
await scene.settled(cmdBar)
|
|
|
|
await u.openAndClearDebugPanel()
|
|
await u.closeKclCodePanel()
|
|
|
|
await test.step('Pan', async () => {
|
|
await bakeInRetries({
|
|
mouseActions: async () => {
|
|
await page.keyboard.down('Shift')
|
|
await page.mouse.move(600, 200)
|
|
await page.mouse.down({ button: 'right' })
|
|
// Gotcha: remove steps:2 from this 700,200 mouse move. This bricked the test on local host engine.
|
|
await page.mouse.move(700, 200)
|
|
await page.mouse.up({ button: 'right' })
|
|
await page.keyboard.up('Shift')
|
|
await page.waitForTimeout(200)
|
|
},
|
|
afterPosition: [19, 85, 85],
|
|
beforePosition: camInitialPosition,
|
|
page,
|
|
scene,
|
|
})
|
|
})
|
|
|
|
await test.step('Zoom with click and drag', async () => {
|
|
await bakeInRetries({
|
|
mouseActions: async () => {
|
|
await page.keyboard.down('Control')
|
|
await page.mouse.move(700, 400)
|
|
await page.mouse.down({ button: 'right' })
|
|
await page.mouse.move(700, 300)
|
|
await page.mouse.up({ button: 'right' })
|
|
await page.keyboard.up('Control')
|
|
},
|
|
afterPosition: [0, 118, 118],
|
|
beforePosition: camInitialPosition,
|
|
page,
|
|
scene,
|
|
})
|
|
})
|
|
|
|
await test.step('Zoom with scrollwheel', async () => {
|
|
const refreshCamValuesCmd: EngineCommand = {
|
|
type: 'modeling_cmd_req',
|
|
cmd_id: uuidv4(),
|
|
cmd: {
|
|
type: 'default_camera_get_settings',
|
|
},
|
|
}
|
|
await bakeInRetries({
|
|
mouseActions: async () => {
|
|
await page.mouse.move(700, 400)
|
|
await page.mouse.wheel(0, -150)
|
|
|
|
// Scroll zooming doesn't update the debug pane's cam position values,
|
|
// so we have to force a refresh.
|
|
await u.openAndClearDebugPanel()
|
|
await u.sendCustomCmd(refreshCamValuesCmd)
|
|
await u.waitForCmdReceive('default_camera_get_settings')
|
|
await u.closeDebugPanel()
|
|
},
|
|
afterPosition: [0, 42.5, 42.5],
|
|
beforePosition: camInitialPosition,
|
|
page,
|
|
scene,
|
|
})
|
|
})
|
|
}
|
|
)
|
|
|
|
test(
|
|
'Can orbit camera reliably',
|
|
{
|
|
tag: '@web',
|
|
},
|
|
async ({ page, homePage, scene, cmdBar }) => {
|
|
const u = await getUtils(page)
|
|
const initialCamPosition: [number, number, number] = [0, 85, 85]
|
|
|
|
await homePage.goToModelingScene()
|
|
// this turns on the debug pane setting as well
|
|
await scene.settled(cmdBar)
|
|
|
|
await u.openAndClearDebugPanel()
|
|
await u.closeKclCodePanel()
|
|
|
|
await test.step('Test orbit with spherical mode', async () => {
|
|
await bakeInRetries({
|
|
mouseActions: async () => {
|
|
await page.mouse.move(700, 200)
|
|
await page.mouse.down({ button: 'right' })
|
|
await page.waitForTimeout(100)
|
|
|
|
const appLogoBBox = await page.getByTestId('app-logo').boundingBox()
|
|
expect(appLogoBBox).not.toBeNull()
|
|
if (!appLogoBBox) throw new Error('app logo not found')
|
|
await page.mouse.move(
|
|
appLogoBBox.x + appLogoBBox.width / 2,
|
|
appLogoBBox.y + appLogoBBox.height / 2
|
|
)
|
|
await page.waitForTimeout(100)
|
|
await page.mouse.move(600, 303)
|
|
await page.waitForTimeout(100)
|
|
await page.mouse.up({ button: 'right' })
|
|
},
|
|
afterPosition: [-4, 10.5, 120],
|
|
beforePosition: initialCamPosition,
|
|
page,
|
|
scene,
|
|
})
|
|
})
|
|
|
|
await test.step('Test orbit with trackball mode', async () => {
|
|
await test.step('Set orbitMode to trackball', async () => {
|
|
await cmdBar.openCmdBar()
|
|
await cmdBar.selectOption({ name: 'camera orbit' }).click()
|
|
await cmdBar.selectOption({ name: 'trackball' }).click()
|
|
await expect(
|
|
page.getByText(`camera orbit to "trackball"`)
|
|
).toBeVisible()
|
|
})
|
|
|
|
await bakeInRetries({
|
|
mouseActions: async () => {
|
|
await page.mouse.move(700, 200)
|
|
await page.mouse.down({ button: 'right' })
|
|
await page.waitForTimeout(100)
|
|
|
|
const appLogoBBox = await page.getByTestId('app-logo').boundingBox()
|
|
expect(appLogoBBox).not.toBeNull()
|
|
if (!appLogoBBox) {
|
|
throw new Error('app logo not found')
|
|
}
|
|
await page.mouse.move(
|
|
appLogoBBox.x + appLogoBBox.width / 2,
|
|
appLogoBBox.y + appLogoBBox.height / 2
|
|
)
|
|
await page.waitForTimeout(100)
|
|
await page.mouse.move(600, 303)
|
|
await page.waitForTimeout(100)
|
|
await page.mouse.up({ button: 'right' })
|
|
},
|
|
afterPosition: [18.06, -42.79, 110.87],
|
|
beforePosition: initialCamPosition,
|
|
page,
|
|
scene,
|
|
})
|
|
})
|
|
}
|
|
)
|
|
|
|
// TODO: fix after electron migration is merged
|
|
test('Zoom should be consistent when exiting or entering sketches', async ({
|
|
page,
|
|
homePage,
|
|
}) => {
|
|
// start new sketch pan and zoom before exiting, when exiting the sketch should stay in the same place
|
|
// than zoom and pan outside of sketch mode and enter again and it should not change from where it is
|
|
// than again for sketching
|
|
|
|
const u = await getUtils(page)
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
|
|
await homePage.goToModelingScene()
|
|
await u.waitForPageLoad()
|
|
await u.openDebugPanel()
|
|
|
|
await expect(
|
|
page.getByRole('button', { name: 'Start Sketch' })
|
|
).not.toBeDisabled()
|
|
await expect(
|
|
page.getByRole('button', { name: 'Start Sketch' })
|
|
).toBeVisible()
|
|
|
|
// click on "Start Sketch" button
|
|
await u.clearCommandLogs()
|
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
|
await page.waitForTimeout(100)
|
|
|
|
// select a plane
|
|
await page.mouse.click(700, 325)
|
|
|
|
let code = `sketch001 = startSketchOn(XY)`
|
|
await expect(u.codeLocator).toHaveText(code)
|
|
await u.closeDebugPanel()
|
|
|
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
|
|
|
// move the camera slightly
|
|
await page.keyboard.down('Shift')
|
|
await page.mouse.move(700, 300)
|
|
await page.mouse.down({ button: 'right' })
|
|
await page.mouse.move(800, 200)
|
|
await page.mouse.up({ button: 'right' })
|
|
await page.keyboard.up('Shift')
|
|
|
|
let y = 350,
|
|
x = 948
|
|
|
|
await u.canvasLocator.click({ position: { x: 783, y } })
|
|
code += `\n |> startProfile(at = [8.12, -12.98])`
|
|
// await expect(u.codeLocator).toHaveText(code)
|
|
await u.canvasLocator.click({ position: { x, y } })
|
|
code += `\n |> line(end = [11.18, 0])`
|
|
// await expect(u.codeLocator).toHaveText(code)
|
|
await u.canvasLocator.click({ position: { x, y: 275 } })
|
|
code += `\n |> line(end = [0, 6.99])`
|
|
// await expect(u.codeLocator).toHaveText(code)
|
|
|
|
// click the line button
|
|
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
|
|
|
const hoverOverNothing = async () => {
|
|
// await u.canvasLocator.hover({position: {x: 700, y: 325}})
|
|
await page.mouse.move(700, 325)
|
|
await page.waitForTimeout(100)
|
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible({
|
|
timeout: 10_000,
|
|
})
|
|
}
|
|
|
|
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
|
|
|
await page.waitForTimeout(200)
|
|
// hover over horizontal line
|
|
await u.canvasLocator.hover({ position: { x: 800, y } })
|
|
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
|
timeout: 10_000,
|
|
})
|
|
await page.waitForTimeout(200)
|
|
|
|
await hoverOverNothing()
|
|
await page.waitForTimeout(200)
|
|
// hover over vertical line
|
|
await u.canvasLocator.hover({ position: { x, y: 325 } })
|
|
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
|
timeout: 10_000,
|
|
})
|
|
|
|
await hoverOverNothing()
|
|
|
|
// click exit sketch
|
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
|
await page.waitForTimeout(400)
|
|
|
|
await hoverOverNothing()
|
|
await page.waitForTimeout(200)
|
|
// hover over horizontal line
|
|
await page.mouse.move(858, y, { steps: 5 })
|
|
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
|
timeout: 10_000,
|
|
})
|
|
|
|
await hoverOverNothing()
|
|
|
|
// hover over vertical line
|
|
await page.mouse.move(x, 325)
|
|
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
|
timeout: 10_000,
|
|
})
|
|
|
|
await hoverOverNothing()
|
|
|
|
// hover over vertical line
|
|
await page.mouse.move(857, y)
|
|
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
|
timeout: 10_000,
|
|
})
|
|
// now click it
|
|
await page.mouse.click(857, y)
|
|
|
|
await expect(
|
|
page.getByRole('button', { name: 'Edit Sketch' })
|
|
).toBeVisible()
|
|
await hoverOverNothing()
|
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
|
|
|
await page.waitForTimeout(400)
|
|
|
|
x = 975
|
|
y = 468
|
|
|
|
await page.waitForTimeout(100)
|
|
await page.mouse.move(x, 419, { steps: 5 })
|
|
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
|
timeout: 10_000,
|
|
})
|
|
|
|
await hoverOverNothing()
|
|
|
|
await page.mouse.move(855, y)
|
|
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
|
timeout: 10_000,
|
|
})
|
|
|
|
await hoverOverNothing()
|
|
|
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
|
await page.waitForTimeout(200)
|
|
|
|
await hoverOverNothing()
|
|
await page.waitForTimeout(200)
|
|
|
|
await page.mouse.move(x, 419)
|
|
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
|
timeout: 10_000,
|
|
})
|
|
|
|
await hoverOverNothing()
|
|
|
|
await page.mouse.move(855, y)
|
|
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
|
timeout: 10_000,
|
|
})
|
|
})
|
|
|
|
test(`Zoom by scroll should not fire while orbiting`, async ({
|
|
homePage,
|
|
page,
|
|
}) => {
|
|
/**
|
|
* Currently we only allow zooming by scroll when no other camera movement is happening,
|
|
* set within cameraMouseDragGuards in cameraControls.ts,
|
|
* until the engine supports unifying multiple camera movements.
|
|
* This verifies that scrollCallback's guard is working as expected.
|
|
*/
|
|
const u = await getUtils(page)
|
|
|
|
// Constants and locators
|
|
const settingsLink = page.getByTestId('settings-link')
|
|
const settingsDialogHeading = page.getByRole('heading', {
|
|
name: 'Settings',
|
|
exact: true,
|
|
})
|
|
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
|
const mouseControlsSetting = () => page.locator('#camera-controls').first()
|
|
const mouseControlSuccessToast = page.getByText(
|
|
'Set mouse controls to "Solidworks"'
|
|
)
|
|
const settingsCloseButton = page.getByTestId('settings-close-button')
|
|
const gizmo = page.locator('[aria-label*=gizmo]')
|
|
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
|
|
const orbitMouseStart = { x: 800, y: 130 }
|
|
const orbitMouseEnd = { x: 0, y: 130 }
|
|
const mid = (v1: number, v2: number) => v1 + (v2 - v1) / 2
|
|
type Point = { x: number; y: number }
|
|
const midPoint = (p1: Point, p2: Point) => ({
|
|
x: mid(p1.x, p2.x),
|
|
y: mid(p1.y, p2.y),
|
|
})
|
|
const orbitMouseStepOne = midPoint(orbitMouseStart, orbitMouseEnd)
|
|
const expectedStartCamZPosition = 64.0
|
|
const expectedZoomCamZPosition = 32.0
|
|
const expectedOrbitCamZPosition = 64.0
|
|
|
|
await test.step(`Test setup`, async () => {
|
|
await homePage.goToModelingScene()
|
|
await u.waitForPageLoad()
|
|
await u.closeKclCodePanel()
|
|
// This test requires the mouse controls to be set to Solidworks
|
|
await u.openDebugPanel()
|
|
await test.step(`Set mouse controls setting to Solidworks`, async () => {
|
|
await settingsLink.click()
|
|
await expect(settingsDialogHeading).toBeVisible()
|
|
await userSettingsTab.click()
|
|
const setting = mouseControlsSetting()
|
|
await expect(setting).toBeAttached()
|
|
await setting.scrollIntoViewIfNeeded()
|
|
await setting.selectOption({ label: 'Solidworks' })
|
|
await expect(setting, 'Setting value did not change').toHaveValue(
|
|
'Solidworks',
|
|
{ timeout: 120_000 }
|
|
)
|
|
await expect(mouseControlSuccessToast).toBeVisible()
|
|
await settingsCloseButton.click()
|
|
})
|
|
})
|
|
|
|
await test.step(`Test scrolling zoom works`, async () => {
|
|
await resetCamera()
|
|
await page.mouse.move(orbitMouseStart.x, orbitMouseStart.y)
|
|
await page.mouse.wheel(0, -100)
|
|
await test.step(`Force a refresh of the camera position`, async () => {
|
|
await u.openAndClearDebugPanel()
|
|
await u.sendCustomCmd({
|
|
type: 'modeling_cmd_req',
|
|
cmd_id: uuidv4(),
|
|
cmd: {
|
|
type: 'default_camera_get_settings',
|
|
},
|
|
})
|
|
await u.waitForCmdReceive('default_camera_get_settings')
|
|
})
|
|
|
|
await expect
|
|
.poll(getCameraZValue, {
|
|
message: 'Camera should be at expected position after zooming',
|
|
})
|
|
.toEqual(expectedZoomCamZPosition)
|
|
})
|
|
|
|
await test.step(`Test orbiting works`, async () => {
|
|
await doOrbitWith()
|
|
})
|
|
|
|
await test.step(`Test scrolling while orbiting doesn't zoom`, async () => {
|
|
await doOrbitWith(async () => {
|
|
await page.mouse.wheel(0, -100)
|
|
})
|
|
})
|
|
|
|
// Helper functions
|
|
async function resetCamera() {
|
|
await test.step(`Reset camera`, async () => {
|
|
await u.openDebugPanel()
|
|
await u.clearCommandLogs()
|
|
await u.doAndWaitForCmd(async () => {
|
|
await gizmo.click({ button: 'right' })
|
|
await resetCameraButton.click()
|
|
}, 'zoom_to_fit')
|
|
await expect
|
|
.poll(getCameraZValue, {
|
|
message: 'Camera Z should be at expected position after reset',
|
|
})
|
|
.toEqual(expectedStartCamZPosition)
|
|
})
|
|
}
|
|
|
|
async function getCameraZValue() {
|
|
return page
|
|
.getByTestId('cam-z-position')
|
|
.inputValue()
|
|
.then((value) => parseFloat(value))
|
|
}
|
|
|
|
async function doOrbitWith(callback = async () => {}) {
|
|
await resetCamera()
|
|
|
|
await test.step(`Perform orbit`, async () => {
|
|
await page.mouse.move(orbitMouseStart.x, orbitMouseStart.y)
|
|
await page.mouse.down({ button: 'middle' })
|
|
await page.mouse.move(orbitMouseStepOne.x, orbitMouseStepOne.y, {
|
|
steps: 3,
|
|
})
|
|
await callback()
|
|
await page.mouse.move(orbitMouseEnd.x, orbitMouseEnd.y, {
|
|
steps: 3,
|
|
})
|
|
})
|
|
|
|
await test.step(`Verify orbit`, async () => {
|
|
await expect
|
|
.poll(getCameraZValue, {
|
|
message: 'Camera should be at expected position after orbiting',
|
|
})
|
|
.toEqual(expectedOrbitCamZPosition)
|
|
await page.mouse.up({ button: 'middle' })
|
|
})
|
|
}
|
|
})
|
|
|
|
test('Right-click opens context menu when not dragged', async ({
|
|
homePage,
|
|
page,
|
|
}) => {
|
|
const u = await getUtils(page)
|
|
|
|
await homePage.goToModelingScene()
|
|
await u.waitForPageLoad()
|
|
|
|
await test.step(`The menu should not show if we drag the mouse`, async () => {
|
|
await page.mouse.move(900, 200)
|
|
await page.mouse.down({ button: 'right' })
|
|
await page.mouse.move(900, 300)
|
|
await page.mouse.up({ button: 'right' })
|
|
|
|
await expect(page.getByTestId('view-controls-menu')).not.toBeVisible()
|
|
})
|
|
|
|
await test.step(`The menu should show if we don't drag the mouse`, async () => {
|
|
await page.mouse.move(900, 200)
|
|
await page.mouse.down({ button: 'right' })
|
|
await page.mouse.up({ button: 'right' })
|
|
|
|
await expect(page.getByTestId('view-controls-menu')).toBeVisible()
|
|
})
|
|
})
|
|
})
|