import { test, expect, Page } from '@playwright/test' import { makeTemplate, getUtils } from './test-utils' import waitOn from 'wait-on' import { roundOff, uuidv4 } from 'lib/utils' import { SaveSettingsPayload } from 'lib/settings/settingsTypes' import { secrets } from './secrets' import { TEST_SETTINGS, TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED, TEST_SETTINGS_ONBOARDING_EXPORT, TEST_SETTINGS_ONBOARDING_START, } from './storageStates' import * as TOML from '@iarna/toml' import { LineInputsType } from 'lang/std/sketchcombos' import { Coords2d } from 'lang/std/sketch' import { KCL_DEFAULT_LENGTH } from 'lib/constants' import { EngineCommand } from 'lang/std/engineConnection' /* debug helper: unfortunately we do rely on exact coord mouse clicks in a few places just from the nature of the stream, running the test with debugger and pasting the below into the console can be useful to get coords document.addEventListener('mousemove', (e) => console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`) ) */ const commonPoints = { startAt: '[9.06, -12.22]', num1: 9.14, num2: 18.2, // num1: 9.64, // num2: 19.19, } test.beforeEach(async ({ context, page }) => { // wait for Vite preview server to be up await waitOn({ resources: ['tcp:3000'], timeout: 5000, }) await context.addInitScript( async ({ token, settingsKey, settings }) => { localStorage.setItem('TOKEN_PERSIST_KEY', token) localStorage.setItem('persistCode', ``) localStorage.setItem(settingsKey, settings) localStorage.setItem('playwright', 'true') }, { token: secrets.token, settingsKey: TEST_SETTINGS_KEY, settings: TOML.stringify({ settings: TEST_SETTINGS }), } ) // kill animations, speeds up tests and reduced flakiness await page.emulateMedia({ reducedMotion: 'reduce' }) }) test.setTimeout(60000) test('Basic sketch', async ({ page }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) const PUR = 400 / 37.5 //pixeltoUnitRatio await page.goto('/') await u.waitForAuthSkipAppStart() 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, 200) await expect(page.locator('.cm-content')).toHaveText( `const part001 = startSketchOn('XZ')` ) await u.closeDebugPanel() await page.waitForTimeout(300) // TODO detect animation ending, or disable animation const startXPx = 600 await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %)`) await page.waitForTimeout(100) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.waitForTimeout(100) await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %) |> line([${commonPoints.num1}, 0], %)`) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %) |> line([${commonPoints.num1}, 0], %) |> line([0, ${commonPoints.num1}], %)`) await page.waitForTimeout(100) await page.mouse.click(startXPx, 500 - PUR * 20) await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %) |> line([${commonPoints.num1}, 0], %) |> line([0, ${commonPoints.num1}], %) |> line([-${commonPoints.num2}, 0], %)`) // deselect line tool await page.getByRole('button', { name: 'Line' }).click() await page.waitForTimeout(100) // click between first two clicks to get center of the line await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10) await page.waitForTimeout(100) // hold down shift await page.keyboard.down('Shift') // click between the latest two clicks to get center of the line await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20) // selected two lines therefore there should be two cursors await expect(page.locator('.cm-cursor')).toHaveCount(2) await page.getByRole('button', { name: 'Constrain' }).click() await page.getByRole('button', { name: 'Equal Length' }).click() await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %) |> line([${commonPoints.num1}, 0], %, 'seg01') |> line([0, ${commonPoints.num1}], %) |> angledLine([180, segLen('seg01', %)], %)`) }) test('Can moving camera', async ({ page, context }) => { test.skip(process.platform === 'darwin', 'Can moving camera') const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() await u.openAndClearDebugPanel() await u.closeKclCodePanel() const camPos: [number, number, number] = [0, 85, 85] const bakeInRetries = async ( mouseActions: any, xyz: [number, number, number], cnt = 0 ) => { // 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 camCommand: EngineCommand = { type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_look_at', center: { x: 0, y: 0, z: 0 }, vantage: { x: camPos[0], y: camPos[1], z: camPos[2] }, up: { x: 0, y: 0, z: 1 }, }, } const updateCamCommand: EngineCommand = { type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_get_settings', }, } await u.sendCustomCmd(camCommand) await page.waitForTimeout(100) await u.sendCustomCmd(updateCamCommand) await page.waitForTimeout(100) // rotate await u.closeDebugPanel() await page.getByRole('button', { name: 'Start Sketch' }).click() await page.waitForTimeout(100) // const yo = page.getByTestId('cam-x-position').inputValue() await u.doAndWaitForImageDiff(async () => { await mouseActions() await u.openAndClearDebugPanel() await u.closeDebugPanel() await page.waitForTimeout(100) }, 300) await u.openAndClearDebugPanel() await page.getByTestId('cam-x-position').isVisible() const vals = await Promise.all([ page.getByTestId('cam-x-position').inputValue(), page.getByTestId('cam-y-position').inputValue(), page.getByTestId('cam-z-position').inputValue(), ]) const xError = Math.abs(Number(vals[0]) + xyz[0]) const yError = Math.abs(Number(vals[1]) + xyz[1]) const zError = Math.abs(Number(vals[2]) + xyz[2]) let shouldRetry = false if (xError > 5 || yError > 5 || zError > 5) { if (cnt > 2) { console.log('xVal', vals[0], 'xError', xError) console.log('yVal', vals[1], 'yError', yError) console.log('zVal', vals[2], 'zError', zError) throw new Error('Camera position not as expected') } shouldRetry = true } await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.waitForTimeout(100) if (shouldRetry) await bakeInRetries(mouseActions, xyz, cnt + 1) } await bakeInRetries(async () => { await page.mouse.move(700, 200) await page.mouse.down({ button: 'right' }) await page.mouse.move(600, 303) await page.mouse.up({ button: 'right' }) }, [4, -10.5, -120]) await bakeInRetries(async () => { await page.keyboard.down('Shift') await page.mouse.move(600, 200) await page.mouse.down({ button: 'right' }) await page.mouse.move(700, 200, { steps: 2 }) await page.mouse.up({ button: 'right' }) await page.keyboard.up('Shift') }, [-19, -85, -85]) const camCommand: EngineCommand = { type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_look_at', center: { x: 0, y: 0, z: 0 }, vantage: { x: camPos[0], y: camPos[1], z: camPos[2] }, up: { x: 0, y: 0, z: 1 }, }, } const updateCamCommand: EngineCommand = { type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_get_settings', }, } await u.sendCustomCmd(camCommand) await page.waitForTimeout(100) await u.sendCustomCmd(updateCamCommand) await page.waitForTimeout(100) await u.clearCommandLogs() await u.closeDebugPanel() await page.getByRole('button', { name: 'Start Sketch' }).click() await page.waitForTimeout(200) // zoom await u.doAndWaitForImageDiff(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') await u.openDebugPanel() await page.waitForTimeout(300) await u.clearCommandLogs() await u.closeDebugPanel() }, 300) // zoom with scroll await u.openAndClearDebugPanel() // TODO, it appears we don't get the cam setting back from the engine when the interaction is zoom into `backInRetries` once the information is sent back on zoom // await expect(Math.abs(Number(await page.getByTestId('cam-x-position').inputValue()) + 12)).toBeLessThan(1.5) // await expect(Math.abs(Number(await page.getByTestId('cam-y-position').inputValue()) - 85)).toBeLessThan(1.5) // await expect(Math.abs(Number(await page.getByTestId('cam-z-position').inputValue()) - 85)).toBeLessThan(1.5) await page.getByRole('button', { name: 'Exit Sketch' }).click() await bakeInRetries(async () => { await page.mouse.move(700, 400) await page.mouse.wheel(0, -100) }, [1, -68, -68]) }) test('if you click the format button it formats your code', async ({ page, }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1000, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // check no error to begin with await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await page.click('.cm-content') await page.keyboard.type(`const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> line([-20, 0], %) |> close(%)`) await page.click('#code-pane button:first-child') await page.click('button:has-text("Format code")') await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> line([-20, 0], %) |> close(%)`) }) test('if you use the format keyboard binding it formats your code', async ({ page, }) => { const u = await getUtils(page) await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> line([-20, 0], %) |> close(%)` ) }) await page.setViewportSize({ width: 1000, height: 500 }) const lspStartPromise = page.waitForEvent('console', async (message) => { // it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]') // but that doesn't seem to make it to the console for macos/safari :( if (message.text().includes('start kcl lsp')) { await new Promise((resolve) => setTimeout(resolve, 200)) return true } return false }) await page.goto('/') await u.waitForAuthSkipAppStart() await lspStartPromise // check no error to begin with await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() // focus the editor await page.click('.cm-line') // Hit alt+shift+f to format the code await page.keyboard.press('Alt+Shift+KeyF') await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> line([-20, 0], %) |> close(%)`) }) test('ensure the Zoo logo is not a link in browser app', async ({ page }) => { await page.setViewportSize({ width: 1000, height: 500 }) await page.goto('/') const zooLogo = page.locator('[data-testid="app-logo"]') // Make sure it's not a link await expect(zooLogo).not.toHaveAttribute('href') }) test('if you write invalid kcl you get inlined errors', async ({ page }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1000, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // check no error to begin with await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() /* add the following code to the editor (# error is not a valid line) # error const topAng = 30 const bottomAng = 25 */ await page.click('.cm-content') await page.keyboard.type('$ error') // press arrows to clear autocomplete await page.keyboard.press('ArrowLeft') await page.keyboard.press('ArrowRight') await page.keyboard.press('Enter') await page.keyboard.type('const topAng = 30') await page.keyboard.press('Enter') await page.keyboard.type('const bottomAng = 25') await page.keyboard.press('Enter') // error in guter await expect(page.locator('.cm-lint-marker-error')).toBeVisible() // error text on hover await page.hover('.cm-lint-marker-error') await expect(page.getByText("found unknown token '$'")).toBeVisible() // select the line that's causing the error and delete it await page.getByText('$ error').click() await page.keyboard.press('End') await page.keyboard.down('Shift') await page.keyboard.press('Home') await page.keyboard.up('Shift') await page.keyboard.press('Backspace') // wait for .cm-lint-marker-error not to be visible await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() // let's check we get an error when defining the same variable twice await page.getByText('const bottomAng = 25').click() await page.keyboard.press('Enter') await page.keyboard.type("// Let's define the same thing twice") await page.keyboard.press('Enter') await page.keyboard.type('const topAng = 42') await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lintRange.cm-lintRange-error')).toBeVisible() await page.locator('.cm-lintRange.cm-lintRange-error').hover() await expect(page.locator('.cm-diagnosticText')).toBeVisible() await expect(page.getByText('Cannot redefine topAng')).toBeVisible() const secondTopAng = await page.getByText('topAng').first() await secondTopAng?.dblclick() await page.keyboard.type('otherAng') await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() }) test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => { const u = await getUtils(page) await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const length = .750 const width = 0.500 const height = 0.500 const dia = 4 fn squareHole = (l, w) => { const squareHoleSketch = startSketchOn('XY') |> startProfileAt([-width / 2, -length / 2], %) |> lineTo([width / 2, -length / 2], %) |> lineTo([width / 2, length / 2], %) |> lineTo([-width / 2, length / 2], %) |> close(%) return squareHoleSketch } ` ) }) await page.setViewportSize({ width: 1000, height: 500 }) await page.goto('/') const lspStartPromise = page.waitForEvent('console', async (message) => { // it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]') // but that doesn't seem to make it to the console for macos/safari :( if (message.text().includes('start kcl lsp')) { await new Promise((resolve) => setTimeout(resolve, 200)) return true } return false }) await page.goto('/') await u.waitForAuthSkipAppStart() await lspStartPromise await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() // check no error to begin with await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() // Click on the bottom of the code editor to add a new line await page.click('.cm-content') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('Enter') await page.keyboard.type(`const extrusion = startSketchOn('XY') |> circle([0, 0], dia/2, %) |> hole(squareHole(length, width, height), %) |> extrude(height, %)`) // error in gutter await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() await page.hover('.cm-lint-marker-error:first-child') await expect(page.getByText('Expected 2 arguments, got 3')).toBeVisible() // Make sure there are two diagnostics await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2) }) test('if your kcl gets an error from the engine it is inlined', async ({ page, }) => { const u = await getUtils(page) await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const box = startSketchOn('XY') |> startProfileAt([0, 0], %) |> line([0, 10], %) |> line([10, 0], %) |> line([0, -10], %, 'revolveAxis') |> close(%) |> extrude(10, %) const sketch001 = startSketchOn(box, "revolveAxis") |> startProfileAt([5, 10], %) |> line([0, -10], %) |> line([2, 0], %) |> line([0, -10], %) |> close(%) |> revolve({ axis: getEdge('revolveAxis', box), angle: 90 }, %) ` ) }) await page.setViewportSize({ width: 1000, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() // error in guter await expect(page.locator('.cm-lint-marker-error')).toBeVisible() // error text on hover await page.hover('.cm-lint-marker-error') await expect( page.getByText( 'sketch profile must lie entirely on one side of the revolution axis' ) ).toBeVisible() }) test('executes on load', async ({ page }) => { const u = await getUtils(page) await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('-XZ') |> startProfileAt([-6.95, 4.98], %) |> line([25.1, 0.41], %) |> line([0.73, -14.93], %) |> line([-23.44, 0.52], %)` ) }) await page.setViewportSize({ width: 1000, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // expand variables section const variablesTabButton = page.getByRole('tab', { name: 'Variables', exact: false, }) await variablesTabButton.click() // can find part001 in the variables summary (pretty-json-container, makes sure we're not looking in the code editor) // part001 only shows up in the variables summary if it's been executed await page.waitForFunction(() => { const variablesElement = document.querySelector( '.pretty-json-container' ) as HTMLDivElement return variablesElement.innerHTML.includes('part001') }) await expect( page.locator('.pretty-json-container >> text=part001') ).toBeVisible() }) test('re-executes', async ({ page }) => { const u = await getUtils(page) await page.addInitScript(async () => { localStorage.setItem('persistCode', `const myVar = 5`) }) await page.setViewportSize({ width: 1000, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() const variablesTabButton = page.getByRole('tab', { name: 'Variables', exact: false, }) await variablesTabButton.click() // expect to see "myVar:5" await expect( page.locator('.pretty-json-container >> text=myVar:5') ).toBeVisible() // change 5 to 67 await page.getByText('const myVar').click() await page.keyboard.press('End') await page.keyboard.press('Backspace') await page.keyboard.type('67') await expect( page.locator('.pretty-json-container >> text=myVar:67') ).toBeVisible() }) const sketchOnPlaneAndBackSideTest = async ( page: any, plane: string, clickCoords: { x: number; y: number } ) => { const u = await getUtils(page) const PUR = 400 / 37.5 //pixeltoUnitRatio await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() await u.openDebugPanel() const coord = plane === '-XY' || plane === '-YZ' || plane === 'XZ' ? -100 : 100 const camCommand: EngineCommand = { type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_look_at', center: { x: 0, y: 0, z: 0 }, vantage: { x: coord, y: coord, z: coord }, up: { x: 0, y: 0, z: 1 }, }, } const updateCamCommand: EngineCommand = { type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_get_settings', }, } const code = `const part001 = startSketchOn('${plane}') |> startProfileAt([1.14, -1.54], %)` await u.openDebugPanel() await u.clearCommandLogs() await page.getByRole('button', { name: 'Start Sketch' }).click() await u.sendCustomCmd(camCommand) await page.waitForTimeout(100) await u.sendCustomCmd(updateCamCommand) await u.closeDebugPanel() await page.mouse.click(clickCoords.x, clickCoords.y) await page.waitForTimeout(300) // wait for animation await expect(page.getByRole('button', { name: 'Line' })).toBeVisible() // draw a line const startXPx = 600 await u.closeDebugPanel() await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await expect(page.locator('.cm-content')).toHaveText(code) await page.getByRole('button', { name: 'Line' }).click() await u.openAndClearDebugPanel() await page.getByRole('button', { name: 'Exit Sketch' }).click() await u.expectCmdLog('[data-message-type="execution-done"]') await u.clearCommandLogs() await u.removeCurrentCode() } test.describe('Can create sketches on all planes and their back sides', () => { test('XY', async ({ page }) => { await sketchOnPlaneAndBackSideTest( page, 'XY', { x: 600, y: 388 } // red plane // { x: 600, y: 400 }, // red plane // clicks grid helper and that causes problems, should fix so that these coords work too. ) }) test('YZ', async ({ page }) => { await sketchOnPlaneAndBackSideTest(page, 'YZ', { x: 700, y: 250 }) // green plane }) test('XZ', async ({ page }) => { await sketchOnPlaneAndBackSideTest(page, '-XZ', { x: 700, y: 80 }) // blue plane }) test('-XY', async ({ page }) => { await sketchOnPlaneAndBackSideTest(page, '-XY', { x: 600, y: 118 }) // back of red plane }) test('-YZ', async ({ page }) => { await sketchOnPlaneAndBackSideTest(page, '-YZ', { x: 700, y: 219 }) // back of green plane }) test('-XZ', async ({ page }) => { await sketchOnPlaneAndBackSideTest(page, 'XZ', { x: 700, y: 427 }) // back of blue plane }) }) test('Auto complete works', async ({ page }) => { const u = await getUtils(page) // const PUR = 400 / 37.5 //pixeltoUnitRatio await page.setViewportSize({ width: 1200, height: 500 }) const lspStartPromise = page.waitForEvent('console', async (message) => { // it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]') // but that doesn't seem to make it to the console for macos/safari :( if (message.text().includes('start kcl lsp')) { await new Promise((resolve) => setTimeout(resolve, 200)) return true } return false }) await page.goto('/') await u.waitForAuthSkipAppStart() await lspStartPromise // this test might be brittle as we add and remove functions // but should also be easy to update. // tests clicking on an option, selection the first option // and arrowing down to an option await page.click('.cm-content') await page.keyboard.type('const part001 = start') // expect there to be six auto complete options await expect(page.locator('.cm-completionLabel')).toHaveCount(6) await page.getByText('startSketchOn').click() await page.keyboard.type("'XZ'") await page.keyboard.press('Tab') await page.keyboard.press('Enter') await page.keyboard.type(' |> startProfi') // expect there be a single auto complete option that we can just hit enter on await expect(page.locator('.cm-completionLabel')).toBeVisible() await page.waitForTimeout(100) await page.keyboard.press('Enter') // accepting the auto complete, not a new line await page.keyboard.press('Tab') await page.keyboard.type('12') await page.waitForTimeout(100) await page.keyboard.press('Tab') await page.waitForTimeout(100) await page.keyboard.press('Tab') await page.keyboard.press('Tab') await page.keyboard.press('Enter') await page.keyboard.type(' |> lin') await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible() await page.waitForTimeout(100) // press arrow down twice then enter to accept xLine await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('Enter') // finish line with comment await page.keyboard.type('5') await page.keyboard.press('Tab') await page.keyboard.press('Tab') await page.keyboard.type(' // lin') await page.waitForTimeout(100) // there shouldn't be any auto complete options for 'lin' in the comment await expect(page.locator('.cm-completionLabel')).not.toBeVisible() await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt([3.14, 12], %) |> xLine(5, %) // lin`) }) test('Stored settings are validated and fall back to defaults', async ({ page, }) => { const u = await getUtils(page) // Override beforeEach test setup // with corrupted settings await page.addInitScript( async ({ settingsKey, settings }) => { localStorage.setItem(settingsKey, settings) }, { settingsKey: TEST_SETTINGS_KEY, settings: TOML.stringify({ settings: TEST_SETTINGS_CORRUPTED }), } ) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // Check the settings were reset const storedSettings = TOML.parse( await page.evaluate( ({ settingsKey }) => localStorage.getItem(settingsKey) || '', { settingsKey: TEST_SETTINGS_KEY } ) ) as { settings: SaveSettingsPayload } expect(storedSettings.settings?.app?.theme).toBe(undefined) // Check that the invalid settings were removed expect(storedSettings.settings?.modeling?.defaultUnit).toBe(undefined) expect(storedSettings.settings?.modeling?.mouseControls).toBe(undefined) expect(storedSettings.settings?.app?.projectDirectory).toBe(undefined) expect(storedSettings.settings?.projects?.defaultProjectName).toBe(undefined) }) test('Project settings can be set and override user settings', async ({ page, }) => { await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/', { waitUntil: 'domcontentloaded' }) await page .getByRole('button', { name: 'Start Sketch' }) .waitFor({ state: 'visible' }) // Open the settings modal with the browser keyboard shortcut await page.keyboard.press('Meta+Shift+,') await expect( page.getByRole('heading', { name: 'Settings', exact: true }) ).toBeVisible() await page .locator('select[name="app-theme"]') .selectOption({ value: 'light' }) // Verify the toast appeared await expect( page.getByText(`Set theme to "light" for this project`) ).toBeVisible() // Check that the theme changed await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) // Check that the user setting was not changed await page.getByRole('radio', { name: 'User' }).click() await expect(page.locator('select[name="app-theme"]')).toHaveValue('dark') // Roll back to default "system" theme await page .getByText( 'themeRoll back themeRoll back to match defaultThe overall appearance of the appl' ) .hover() await page .getByRole('button', { name: 'Roll back theme ; Has tooltip: Roll back to match default', }) .click() await expect(page.locator('select[name="app-theme"]')).toHaveValue('system') // Check that the project setting did not change await page.getByRole('radio', { name: 'Project' }).click() await expect(page.locator('select[name="app-theme"]')).toHaveValue('light') }) test('Project settings can be opened with keybinding from the editor', async ({ page, }) => { await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/', { waitUntil: 'domcontentloaded' }) await page .getByRole('button', { name: 'Start Sketch' }) .waitFor({ state: 'visible' }) // Put the cursor in the editor await page.click('.cm-content') // Open the settings modal with the browser keyboard shortcut await page.keyboard.press('Meta+Shift+,') await expect( page.getByRole('heading', { name: 'Settings', exact: true }) ).toBeVisible() await page .locator('select[name="app-theme"]') .selectOption({ value: 'light' }) // Verify the toast appeared await expect( page.getByText(`Set theme to "light" for this project`) ).toBeVisible() // Check that the theme changed await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) // Check that the user setting was not changed await page.getByRole('radio', { name: 'User' }).click() await expect(page.locator('select[name="app-theme"]')).toHaveValue('dark') // Roll back to default "system" theme await page .getByText( 'themeRoll back themeRoll back to match defaultThe overall appearance of the appl' ) .hover() await page .getByRole('button', { name: 'Roll back theme ; Has tooltip: Roll back to match default', }) .click() await expect(page.locator('select[name="app-theme"]')).toHaveValue('system') // Check that the project setting did not change await page.getByRole('radio', { name: 'Project' }).click() await expect(page.locator('select[name="app-theme"]')).toHaveValue('light') }) test('Project and user settings can be reset', async ({ page }) => { await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/', { waitUntil: 'domcontentloaded' }) await page .getByRole('button', { name: 'Start Sketch' }) .waitFor({ state: 'visible' }) // Put the cursor in the editor await page.click('.cm-content') // Open the settings modal with the browser keyboard shortcut await page.keyboard.press('Meta+Shift+,') await expect( page.getByRole('heading', { name: 'Settings', exact: true }) ).toBeVisible() // Click the reset settings button. await page.getByRole('button', { name: 'Restore default settings' }).click() await page .locator('select[name="app-theme"]') .selectOption({ value: 'light' }) // Verify the toast appeared await expect( page.getByText(`Set theme to "light" for this project`) ).toBeVisible() // Check that the theme changed await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) await expect(page.locator('select[name="app-theme"]')).toHaveValue('light') // Check that the user setting was not changed await page.getByRole('radio', { name: 'User' }).click() await expect(page.locator('select[name="app-theme"]')).toHaveValue('system') // Click the reset settings button. await page.getByRole('button', { name: 'Restore default settings' }).click() // Verify it is now set to the default value await expect(page.locator('select[name="app-theme"]')).toHaveValue('system') // Set the user theme to light. await page .locator('select[name="app-theme"]') .selectOption({ value: 'light' }) // Verify the toast appeared await expect( page.getByText(`Set theme to "light" as a user default`) ).toBeVisible() // Check that the theme changed await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) await expect(page.locator('select[name="app-theme"]')).toHaveValue('light') await page.getByRole('radio', { name: 'Project' }).click() await expect(page.locator('select[name="app-theme"]')).toHaveValue('light') // Click the reset settings button. await page.getByRole('button', { name: 'Restore default settings' }).click() // Verify it is now set to the default value await expect(page.locator('select[name="app-theme"]')).toHaveValue('system') await page.getByRole('radio', { name: 'User' }).click() await expect(page.locator('select[name="app-theme"]')).toHaveValue('system') // Click the reset settings button. await page.getByRole('button', { name: 'Restore default settings' }).click() // Verify it is now set to the default value await expect(page.locator('select[name="app-theme"]')).toHaveValue('system') }) test('Click through each onboarding step', async ({ page }) => { const u = await getUtils(page) // Override beforeEach test setup await page.addInitScript( async ({ settingsKey, settings }) => { // Give no initial code, so that the onboarding start is shown immediately localStorage.setItem('persistCode', '') localStorage.setItem(settingsKey, settings) }, { settingsKey: TEST_SETTINGS_KEY, settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_START }), } ) await page.setViewportSize({ width: 1200, height: 1080 }) await page.goto('/') await u.waitForAuthSkipAppStart() // Test that the onboarding pane loaded await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible() const nextButton = page.getByTestId('onboarding-next') while ((await nextButton.innerText()) !== 'Finish') { await expect(nextButton).toBeVisible() await nextButton.click() } // Finish the onboarding await expect(nextButton).toBeVisible() await nextButton.click() // Test that the onboarding pane is gone await expect(page.getByTestId('onboarding-content')).not.toBeVisible() await expect(page.url()).not.toContain('onboarding') }) test('Onboarding redirects and code updating', async ({ page }) => { const u = await getUtils(page) // Override beforeEach test setup await page.addInitScript( async ({ settingsKey, settings }) => { // Give some initial code, so we can test that it's cleared localStorage.setItem('persistCode', 'const sigmaAllow = 15000') localStorage.setItem(settingsKey, settings) }, { settingsKey: TEST_SETTINGS_KEY, settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_EXPORT }), } ) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // Test that the redirect happened await expect(page.url().split(':3000').slice(-1)[0]).toBe( `/file/%2Fbrowser%2Fmain.kcl/onboarding/export` ) // Test that you come back to this page when you refresh await page.reload() await expect(page.url().split(':3000').slice(-1)[0]).toBe( `/file/%2Fbrowser%2Fmain.kcl/onboarding/export` ) // Test that the onboarding pane loaded const title = page.locator('[data-testid="onboarding-content"]') await expect(title).toBeAttached() // Test that the code changes when you advance to the next step await page.locator('[data-testid="onboarding-next"]').click() await expect(page.locator('.cm-content')).toHaveText('') // Test that the code is not empty when you click on the next step await page.locator('[data-testid="onboarding-next"]').click() await expect(page.locator('.cm-content')).toHaveText(/.+/) }) test('Selections work on fresh and edited sketch', async ({ page }) => { // tests mapping works on fresh sketch and edited sketch // tests using hovers which is the same as selections, because if // source ranges are wrong, hovers won't work const u = await getUtils(page) const PUR = 400 / 37.5 //pixeltoUnitRatio await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() await u.openDebugPanel() const xAxisClick = () => page.mouse.click(700, 253).then(() => page.waitForTimeout(100)) const emptySpaceClick = () => page.mouse.click(700, 343).then(() => page.waitForTimeout(100)) const topHorzSegmentClick = () => page.mouse.click(709, 290).then(() => page.waitForTimeout(100)) const bottomHorzSegmentClick = () => page.mouse.click(767, 396).then(() => page.waitForTimeout(100)) await u.clearCommandLogs() await expect( page.getByRole('button', { name: 'Start Sketch' }) ).not.toBeDisabled() await page.getByRole('button', { name: 'Start Sketch' }).click() // select a plane await page.mouse.click(700, 200) await page.waitForTimeout(700) // wait for animation const startXPx = 600 await u.closeDebugPanel() await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %)`) await page.waitForTimeout(100) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %) |> line([${commonPoints.num1}, 0], %)`) await page.waitForTimeout(100) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %) |> line([${commonPoints.num1}, 0], %) |> line([0, ${commonPoints.num1}], %)`) await page.waitForTimeout(100) await page.mouse.click(startXPx, 500 - PUR * 20) await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %) |> line([${commonPoints.num1}, 0], %) |> line([0, ${commonPoints.num1}], %) |> line([-${commonPoints.num2}, 0], %)`) // deselect line tool await page.getByRole('button', { name: 'Line' }).click() await u.closeDebugPanel() const selectionSequence = async (isSecondTime = false) => { await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await page.waitForTimeout(100) await page.mouse.move( startXPx + PUR * 15, isSecondTime ? 430 : 500 - PUR * 10 ) await expect(page.getByTestId('hover-highlight')).toBeVisible() // bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience // and will be an easy fix if it breaks because we change the colour await expect(page.locator('.bg-yellow-200')).toBeVisible() // check mousing off, than mousing onto another line await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await page.mouse.move( startXPx + PUR * 10, isSecondTime ? 295 : 500 - PUR * 20 ) // mouse onto another line await expect(page.getByTestId('hover-highlight')).toBeVisible() // now check clicking works including axis // click a segment hold shift and click an axis, see that a relevant constraint is enabled await topHorzSegmentClick() await page.keyboard.down('Shift') const constrainButton = page.getByRole('button', { name: 'Constrain' }) const absYButton = page.getByRole('button', { name: 'ABS Y' }) await constrainButton.click() await expect(absYButton).toBeDisabled() await page.waitForTimeout(100) await xAxisClick() await page.keyboard.up('Shift') await constrainButton.click() await absYButton.and(page.locator(':not([disabled])')).waitFor() await expect(absYButton).not.toBeDisabled() // clear selection by clicking on nothing await emptySpaceClick() await page.waitForTimeout(100) // same selection but click the axis first await xAxisClick() await constrainButton.click() await expect(absYButton).toBeDisabled() await page.keyboard.down('Shift') await page.waitForTimeout(100) await topHorzSegmentClick() await page.waitForTimeout(100) await page.keyboard.up('Shift') await constrainButton.click() await expect(absYButton).not.toBeDisabled() // clear selection by clicking on nothing await emptySpaceClick() // check the same selection again by putting cursor in code first then selecting axis await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click() await page.keyboard.down('Shift') await constrainButton.click() await expect(absYButton).toBeDisabled() await page.waitForTimeout(100) await xAxisClick() await page.keyboard.up('Shift') await constrainButton.click() await expect(absYButton).not.toBeDisabled() // clear selection by clicking on nothing await emptySpaceClick() // select segment in editor than another segment in scene and check there are two cursors // TODO change this back to shift click in the scene, not cmd click in the editor await bottomHorzSegmentClick() await expect(page.locator('.cm-cursor')).toHaveCount(1) await page.keyboard.down(process.platform === 'linux' ? 'Control' : 'Meta') await page.waitForTimeout(100) await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click() await expect(page.locator('.cm-cursor')).toHaveCount(2) await page.waitForTimeout(500) await page.keyboard.up(process.platform === 'linux' ? 'Control' : 'Meta') // clear selection by clicking on nothing await emptySpaceClick() } await selectionSequence() // hovering in fresh sketch worked, lets try exiting and re-entering await u.openAndClearDebugPanel() await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.waitForTimeout(200) // wait for execution done await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() // select a line // await topHorzSegmentClick() await page.getByText(commonPoints.startAt).click() // TODO remove this and reinstate // await topHorzSegmentClick() await page.waitForTimeout(100) // enter sketch again await u.doAndWaitForCmd( () => page.getByRole('button', { name: 'Edit Sketch' }).click(), 'default_camera_get_settings' ) await page.waitForTimeout(150) await page.waitForTimeout(300) // wait for animation // hover again and check it works await selectionSequence(true) }) test.describe('Command bar tests', () => { test('Command bar works and can change a setting', async ({ page }) => { // Brief boilerplate await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/', { waitUntil: 'domcontentloaded' }) let cmdSearchBar = page.getByPlaceholder('Search commands') // First try opening the command bar and closing it await page .getByRole('button', { name: 'Commands', exact: false }) .or(page.getByRole('button', { name: '⌘K' })) .click() await expect(cmdSearchBar).toBeVisible() await page.keyboard.press('Escape') await expect(cmdSearchBar).not.toBeVisible() // Now try the same, but with the keyboard shortcut, check focus await page.keyboard.press('Meta+K') await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeFocused() // Try typing in the command bar await page.keyboard.type('theme') const themeOption = page.getByRole('option', { name: 'Settings · app · theme', }) await expect(themeOption).toBeVisible() await themeOption.click() const themeInput = page.getByPlaceholder('Select an option') await expect(themeInput).toBeVisible() await expect(themeInput).toBeFocused() // Select dark theme await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute( 'data-headlessui-state', 'active' ) await page.keyboard.press('Enter') // Check the toast appeared await expect( page.getByText(`Set theme to "system" for this project`) ).toBeVisible() // Check that the theme changed await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) }) test('Command bar keybinding works from code editor and can change a setting', async ({ page, }) => { // Brief boilerplate await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/', { waitUntil: 'domcontentloaded' }) let cmdSearchBar = page.getByPlaceholder('Search commands') // Put the cursor in the code editor await page.click('.cm-content') // Now try the same, but with the keyboard shortcut, check focus await page.keyboard.press('Meta+K') await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeFocused() // Try typing in the command bar await page.keyboard.type('theme') const themeOption = page.getByRole('option', { name: 'Settings · app · theme', }) await expect(themeOption).toBeVisible() await themeOption.click() const themeInput = page.getByPlaceholder('Select an option') await expect(themeInput).toBeVisible() await expect(themeInput).toBeFocused() // Select dark theme await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown') await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute( 'data-headlessui-state', 'active' ) await page.keyboard.press('Enter') // Check the toast appeared await expect( page.getByText(`Set theme to "system" for this project`) ).toBeVisible() // Check that the theme changed await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) }) test('Can extrude from the command bar', async ({ page }) => { await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const distance = sqrt(20) const part001 = startSketchOn('XZ') |> startProfileAt([-6.95, 10.98], %) |> line([25.1, 0.41], %) |> line([0.73, -20.93], %) |> line([-23.44, 0.52], %) |> close(%) ` ) }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // Make sure the stream is up await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await expect( page.getByRole('button', { name: 'Start Sketch' }) ).not.toBeDisabled() await u.clearCommandLogs() await page.getByRole('button', { name: 'Extrude' }).isEnabled() let cmdSearchBar = page.getByPlaceholder('Search commands') await page.keyboard.press('Meta+K') await expect(cmdSearchBar).toBeVisible() // Search for extrude command and choose it await page.getByRole('option', { name: 'Extrude' }).click() // Assert that we're on the selection step await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled() // Select a face await page.mouse.move(700, 200) await page.mouse.click(700, 200) // Assert that we're on the distance step await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled() // Assert that the an alternative variable name is chosen, // since the default variable name is already in use (distance) await page.getByRole('button', { name: 'Create new variable' }).click() await expect(page.getByPlaceholder('Variable name')).toHaveValue( 'distance001' ) const continueButton = page.getByRole('button', { name: 'Continue' }) const submitButton = page.getByRole('button', { name: 'Submit command' }) await continueButton.click() // Review step and argument hotkeys await expect(submitButton).toBeEnabled() await page.keyboard.press('Backspace') // Assert we're back on the distance step await expect( page.getByRole('button', { name: 'Distance 5', exact: false }) ).toBeDisabled() await continueButton.click() await submitButton.click() // Check that the code was updated await u.waitForCmdReceive('extrude') // Unfortunately this indentation seems to matter for the test await expect(page.locator('.cm-content')).toHaveText( `const distance = sqrt(20) const distance001 = ${KCL_DEFAULT_LENGTH} const part001 = startSketchOn('XZ') |> startProfileAt([-6.95, 10.98], %) |> line([25.1, 0.41], %) |> line([0.73, -20.93], %) |> line([-23.44, 0.52], %) |> close(%) |> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines ) }) }) test('Can add multiple sketches', async ({ page }) => { test.skip(process.platform === 'darwin', 'Can add multiple sketches') const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) const PUR = 400 / 37.5 //pixeltoUnitRatio await page.goto('/') await u.waitForAuthSkipAppStart() 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 u.doAndWaitForImageDiff( () => page.getByRole('button', { name: 'Start Sketch' }).click(), 200 ) // select a plane await page.mouse.click(700, 200) await expect(page.locator('.cm-content')).toHaveText( `const part001 = startSketchOn('XZ')` ) await page.waitForTimeout(500) // TODO detect animation ending, or disable animation const startXPx = 600 await u.closeDebugPanel() await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %)`) await page.waitForTimeout(100) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.waitForTimeout(100) await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %) |> line([${commonPoints.num1}, 0], %)`) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %) |> line([${commonPoints.num1}, 0], %) |> line([0, ${commonPoints.num1}], %)`) await page.waitForTimeout(100) await page.mouse.click(startXPx, 500 - PUR * 20) const finalCodeFirstSketch = `const part001 = startSketchOn('XZ') |> startProfileAt(${commonPoints.startAt}, %) |> line([${commonPoints.num1}, 0], %) |> line([0, ${commonPoints.num1}], %) |> line([-${commonPoints.num2}, 0], %)` await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch) // exit the sketch await u.openAndClearDebugPanel() await page.getByRole('button', { name: 'Exit Sketch' }).click() await u.expectCmdLog('[data-message-type="execution-done"]') await u.updateCamPosition([100, 100, 100]) await page.waitForTimeout(250) // start a new sketch await u.clearCommandLogs() await page.getByRole('button', { name: 'Start Sketch' }).click() await page.waitForTimeout(400) await page.mouse.click(650, 450) await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await u.clearAndCloseDebugPanel() // on mock os there are issues with getting the camera to update // it should not be selecting the 'XZ' plane here if the camera updated // properly, but if we just role with it we can still verify everything // in the rest of the test const plane = process.platform === 'darwin' ? 'XZ' : 'XY' await page.waitForTimeout(100) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) const startAt2 = process.platform === 'darwin' ? '[9.75, -13.16]' : '[0.93, -1.25]' await expect( (await page.locator('.cm-content').innerText()).replace(/\s/g, '') ).toBe( `${finalCodeFirstSketch} const part002 = startSketchOn('${plane}') |> startProfileAt(${startAt2}, %)`.replace(/\s/g, '') ) await page.waitForTimeout(100) await u.closeDebugPanel() await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.waitForTimeout(100) const num2 = process.platform === 'darwin' ? 9.84 : 0.94 await expect( (await page.locator('.cm-content').innerText()).replace(/\s/g, '') ).toBe( `${finalCodeFirstSketch} const part002 = startSketchOn('${plane}') |> startProfileAt(${startAt2}, %) |> line([${num2}, 0], %)`.replace(/\s/g, '') ) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await expect( (await page.locator('.cm-content').innerText()).replace(/\s/g, '') ).toBe( `${finalCodeFirstSketch} const part002 = startSketchOn('${plane}') |> startProfileAt(${startAt2}, %) |> line([${num2}, 0], %) |> line([0, ${roundOff( num2 + (process.platform === 'darwin' ? 0.01 : -0.01) )}], %)`.replace(/\s/g, '') ) await page.waitForTimeout(100) await page.mouse.click(startXPx, 500 - PUR * 20) await expect( (await page.locator('.cm-content').innerText()).replace(/\s/g, '') ).toBe( `${finalCodeFirstSketch} const part002 = startSketchOn('${plane}') |> startProfileAt(${startAt2}, %) |> line([${num2}, 0], %) |> line([0, ${roundOff( num2 + (process.platform === 'darwin' ? 0.01 : -0.01) )}], %) |> line([-${process.platform === 'darwin' ? 19.59 : 1.87}, 0], %)`.replace( /\s/g, '' ) ) }) test('ProgramMemory can be serialised', async ({ page }) => { const u = await getUtils(page) await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const part = startSketchOn('XY') |> startProfileAt([0, 0], %) |> line([0, 1], %) |> line([1, 0], %) |> line([0, -1], %) |> close(%) |> extrude(1, %) |> patternLinear3d({ axis: [1, 0, 1], repetitions: 3, distance: 6 }, %)` ) }) await page.setViewportSize({ width: 1000, height: 500 }) await page.goto('/') const messages: string[] = [] // Listen for all console events and push the message text to an array page.on('console', (message) => messages.push(message.text())) await u.waitForAuthSkipAppStart() // wait for execution done await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') const forbiddenMessages = ['cannot serialize tagged newtype variant'] forbiddenMessages.forEach((forbiddenMessage) => { messages.forEach((message) => { expect(message).not.toContain(forbiddenMessage) }) }) }) test('Hovering over 3d features highlights code', async ({ page }) => { const u = await getUtils(page) await page.addInitScript(async (KCL_DEFAULT_LENGTH) => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XZ') |> startProfileAt([20, 0], %) |> line([7.13, 4 + 0], %) |> angledLine({ angle: 3 + 0, length: 3.14 + 0 }, %) |> lineTo([20.14 + 0, -0.14 + 0], %) |> xLineTo(29 + 0, %) |> yLine(-3.14 + 0, %, 'a') |> xLine(1.63, %) |> angledLineOfXLength({ angle: 3 + 0, length: 3.14 }, %) |> angledLineOfYLength({ angle: 30, length: 3 + 0 }, %) |> angledLineToX({ angle: 22.14 + 0, to: 12 }, %) |> angledLineToY({ angle: 30, to: 11.14 }, %) |> angledLineThatIntersects({ angle: 3.14, intersectTag: 'a', offset: 0 }, %) |> tangentialArcTo([13.14 + 0, 13.14], %) |> close(%) |> extrude(5 + 7, %) ` ) }, KCL_DEFAULT_LENGTH) await page.setViewportSize({ width: 1000, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // wait for execution done await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() await u.openAndClearDebugPanel() await u.sendCustomCmd({ type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_look_at', vantage: { x: 0, y: -1250, z: 580 }, center: { x: 0, y: 0, z: 0 }, up: { x: 0, y: 0, z: 1 }, }, }) await page.waitForTimeout(100) await u.sendCustomCmd({ type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_get_settings', }, }) await page.waitForTimeout(100) const extrusionTop: Coords2d = [800, 240] const flatExtrusionFace: Coords2d = [960, 160] const arc: Coords2d = [840, 160] const close: Coords2d = [720, 200] const nothing: Coords2d = [600, 200] await page.mouse.move(nothing[0], nothing[1]) await page.mouse.click(nothing[0], nothing[1]) await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await page.waitForTimeout(200) await page.mouse.move(extrusionTop[0], extrusionTop[1]) await expect(page.getByTestId('hover-highlight')).toBeVisible() await page.mouse.move(nothing[0], nothing[1]) await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await page.mouse.move(arc[0], arc[1]) await expect(page.getByTestId('hover-highlight')).toBeVisible() await page.mouse.move(nothing[0], nothing[1]) await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await page.mouse.move(close[0], close[1]) await expect(page.getByTestId('hover-highlight')).toBeVisible() await page.mouse.move(nothing[0], nothing[1]) await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await page.mouse.move(flatExtrusionFace[0], flatExtrusionFace[1]) await expect(page.getByTestId('hover-highlight')).toHaveCount(5) // multiple lines await page.mouse.move(nothing[0], nothing[1]) await page.waitForTimeout(100) await expect(page.getByTestId('hover-highlight')).not.toBeVisible() }) test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({ page, }) => { const u = await getUtils(page) const selectionsSnippets = { extrudeAndEditBlocked: '|> startProfileAt([10.81, 32.99], %)', extrudeAndEditBlockedInFunction: '|> startProfileAt(pos, %)', extrudeAndEditAllowed: '|> startProfileAt([15.72, 4.7], %)', editOnly: '|> startProfileAt([15.79, -14.6], %)', } await page.addInitScript( async ({ extrudeAndEditBlocked, extrudeAndEditBlockedInFunction, extrudeAndEditAllowed, editOnly, }: any) => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XZ') ${extrudeAndEditBlocked} |> line([25.96, 2.93], %) |> line([5.25, -5.72], %) |> line([-2.01, -10.35], %) |> line([-27.65, -2.78], %) |> close(%) |> extrude(5, %) const part002 = startSketchOn('XZ') ${extrudeAndEditAllowed} |> line([10.32, 6.47], %) |> line([9.71, -6.16], %) |> line([-3.08, -9.86], %) |> line([-12.02, -1.54], %) |> close(%) const part003 = startSketchOn('XZ') ${editOnly} |> line([27.55, -1.65], %) |> line([4.95, -8], %) |> line([-20.38, -10.12], %) |> line([-15.79, 17.08], %) fn yohey = (pos) => { const part004 = startSketchOn('XZ') ${extrudeAndEditBlockedInFunction} |> line([27.55, -1.65], %) |> line([4.95, -10.53], %) |> line([-20.38, -8], %) |> line([-15.79, 17.08], %) return '' } yohey([15.79, -34.6]) ` ) }, selectionsSnippets ) await page.setViewportSize({ width: 1200, height: 1000 }) await page.goto('/') await u.waitForAuthSkipAppStart() // wait for execution done await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() // wait for start sketch as a proxy for the stream being ready await expect( page.getByRole('button', { name: 'Start Sketch' }) ).not.toBeDisabled() await page.getByText(selectionsSnippets.extrudeAndEditBlocked).click() await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() await expect( page.getByRole('button', { name: 'Edit Sketch' }) ).not.toBeVisible() await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click() await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled() await expect( page.getByRole('button', { name: 'Edit Sketch' }) ).not.toBeDisabled() await page.getByText(selectionsSnippets.editOnly).click() await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() await expect( page.getByRole('button', { name: 'Edit Sketch' }) ).not.toBeDisabled() await page .getByText(selectionsSnippets.extrudeAndEditBlockedInFunction) .click() await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() await expect( page.getByRole('button', { name: 'Edit Sketch' }) ).not.toBeVisible() // selecting an editable sketch but clicking "start sktech" should start a new sketch and not edit the existing one await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click() await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByTestId('KCL Code').click() await page.mouse.click(734, 134) await page.getByTestId('KCL Code').click() // expect main content to contain `part005` i.e. started a new sketch await expect(page.locator('.cm-content')).toHaveText( /part005 = startSketchOn\('XZ'\)/ ) }) test('Deselecting line tool should mean nothing happens on click', async ({ page, }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() 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 u.doAndWaitForImageDiff( () => page.getByRole('button', { name: 'Start Sketch' }).click(), 200 ) await page.mouse.click(700, 200) await expect(page.locator('.cm-content')).toHaveText( `const part001 = startSketchOn('XZ')` ) await page.waitForTimeout(600) let previousCodeContent = await page.locator('.cm-content').innerText() // deselect the line tool by clicking it await page.getByRole('button', { name: 'Line' }).click() await page.mouse.click(700, 200) await page.waitForTimeout(100) await page.mouse.click(700, 250) await page.waitForTimeout(100) await page.mouse.click(750, 200) await page.waitForTimeout(100) // expect no change await expect(page.locator('.cm-content')).toHaveText(previousCodeContent) // select line tool again await page.getByRole('button', { name: 'Line' }).click() await u.closeDebugPanel() // line tool should work as expected again await page.mouse.click(700, 200) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) previousCodeContent = await page.locator('.cm-content').innerText() await page.waitForTimeout(100) await page.mouse.click(700, 300) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) previousCodeContent = await page.locator('.cm-content').innerText() await page.waitForTimeout(100) await page.mouse.click(750, 300) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) previousCodeContent = await page.locator('.cm-content').innerText() }) test('multi-sketch file shows multiple Edit Sketch buttons', async ({ page, context, }) => { const u = await getUtils(page) const selectionsSnippets = { startProfileAt1: '|> startProfileAt([-width / 4 + screwRadius, height / 2], %)', startProfileAt2: '|> startProfileAt([-width / 2, 0], %)', startProfileAt3: '|> startProfileAt([0, thickness], %)', } await context.addInitScript( async ({ startProfileAt1, startProfileAt2, startProfileAt3 }: any) => { localStorage.setItem( 'persistCode', ` const width = 20 const height = 10 const thickness = 5 const screwRadius = 3 const wireRadius = 2 const wireOffset = 0.5 const screwHole = startSketchOn('XY') ${startProfileAt1} |> arc({ radius: screwRadius, angle_start: 0, angle_end: 360 }, %) const part001 = startSketchOn('XY') ${startProfileAt2} |> xLine(width * .5, %) |> yLine(height, %) |> xLine(-width * .5, %) |> close(%) |> hole(screwHole, %) |> extrude(thickness, %) const part002 = startSketchOn('-XZ') ${startProfileAt3} |> xLine(width / 4, %) |> tangentialArcTo([width / 2, 0], %) |> xLine(-width / 4 + wireRadius, %) |> yLine(wireOffset, %) |> arc({ radius: wireRadius, angle_start: 0, angle_end: 180 }, %) |> yLine(-wireOffset, %) |> xLine(-width / 4, %) |> close(%) |> extrude(-height, %) ` ) }, selectionsSnippets ) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // wait for execution done await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() await page.getByText(selectionsSnippets.startProfileAt1).click() await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await page.getByText(selectionsSnippets.startProfileAt2).click() await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await page.getByText(selectionsSnippets.startProfileAt3).click() await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() }) test('Can edit segments by dragging their handles', async ({ page }) => { const u = await getUtils(page) await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XZ') |> startProfileAt([4.61, -14.01], %) |> line([12.73, -0.09], %) |> tangentialArcTo([24.95, -5.38], %)` ) }) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() await expect( page.getByRole('button', { name: 'Start Sketch' }) ).not.toBeDisabled() await page.waitForTimeout(100) await u.openAndClearDebugPanel() await u.sendCustomCmd({ type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_look_at', vantage: { x: 0, y: -1250, z: 580 }, center: { x: 0, y: 0, z: 0 }, up: { x: 0, y: 0, z: 1 }, }, }) await page.waitForTimeout(100) await u.sendCustomCmd({ type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_get_settings', }, }) await page.waitForTimeout(100) const startPX = [665, 458] const dragPX = 30 await page.getByText('startProfileAt([4.61, -14.01], %)').click() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.waitForTimeout(400) let prevContent = await page.locator('.cm-content').innerText() const step5 = { steps: 5 } await expect(page.getByTestId('segment-overlay')).toHaveCount(2) // drag startProfieAt handle await page.mouse.move(startPX[0], startPX[1]) await page.mouse.down() await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5) await page.mouse.up() await expect(page.locator('.cm-content')).not.toHaveText(prevContent) prevContent = await page.locator('.cm-content').innerText() // drag line handle await page.waitForTimeout(100) const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]') await page.mouse.move(lineEnd.x - 5, lineEnd.y) await page.mouse.down() await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5) await page.mouse.up() await page.waitForTimeout(100) await expect(page.locator('.cm-content')).not.toHaveText(prevContent) prevContent = await page.locator('.cm-content').innerText() // drag tangentialArcTo handle const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]') await page.mouse.move(tangentEnd.x, tangentEnd.y - 5) await page.mouse.down() await page.mouse.move(tangentEnd.x + dragPX, tangentEnd.y - dragPX, step5) await page.mouse.up() await page.waitForTimeout(100) await expect(page.locator('.cm-content')).not.toHaveText(prevContent) // expect the code to have changed await expect(page.locator('.cm-content')) .toHaveText(`const part001 = startSketchOn('XZ') |> startProfileAt([6.44, -12.07], %) |> line([14.72, 1.97], %) |> tangentialArcTo([26.92, -3.32], %)`) }) const doSnapAtDifferentScales = async ( page: any, camPos: [number, number, number], scale = 1, fudge = 0 ) => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() await u.openDebugPanel() const code = `const part001 = startSketchOn('-XZ') |> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %) |> line([${roundOff(scale * 175.36)}, 0], %) |> line([0, -${roundOff(scale * 175.36) + fudge}], %) |> lineTo([profileStartX(%), profileStartY(%)], %) |> close(%)` await expect( page.getByRole('button', { name: 'Start Sketch' }) ).not.toBeDisabled() await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible() await u.clearCommandLogs() await page.getByRole('button', { name: 'Start Sketch' }).click() await page.waitForTimeout(100) await u.openAndClearDebugPanel() await u.updateCamPosition(camPos) await u.closeDebugPanel() // select a plane await page.mouse.click(700, 200) await expect(page.locator('.cm-content')).toHaveText( `const part001 = startSketchOn('-XZ')` ) let prevContent = await page.locator('.cm-content').innerText() const pointA = [700, 200] const pointB = [900, 200] const pointC = [900, 400] // draw three lines await page.mouse.click(pointA[0], pointA[1]) await page.waitForTimeout(100) await expect(page.locator('.cm-content')).not.toHaveText(prevContent) prevContent = await page.locator('.cm-content').innerText() await page.mouse.click(pointB[0], pointB[1]) await page.waitForTimeout(100) await expect(page.locator('.cm-content')).not.toHaveText(prevContent) prevContent = await page.locator('.cm-content').innerText() await page.mouse.click(pointC[0], pointC[1]) await page.waitForTimeout(100) await expect(page.locator('.cm-content')).not.toHaveText(prevContent) prevContent = await page.locator('.cm-content').innerText() await page.mouse.move(pointA[0] - 12, pointA[1] + 12) const pointNotQuiteA = [pointA[0] - 7, pointA[1] + 7] await page.mouse.move(pointNotQuiteA[0], pointNotQuiteA[1], { steps: 10 }) await page.mouse.click(pointNotQuiteA[0], pointNotQuiteA[1]) await expect(page.locator('.cm-content')).not.toHaveText(prevContent) prevContent = await page.locator('.cm-content').innerText() await expect(page.locator('.cm-content')).toHaveText(code) // Assert the tool was unequipped await expect(page.getByRole('button', { name: 'Line' })).not.toHaveAttribute( 'aria-pressed', 'true' ) // exit sketch await u.openAndClearDebugPanel() await page.getByRole('button', { name: 'Exit Sketch' }).click() await u.expectCmdLog('[data-message-type="execution-done"]') await u.removeCurrentCode() } test.describe('Snap to close works (at any scale)', () => { test('[0, 100, 100]', async ({ page }) => { await doSnapAtDifferentScales(page, [0, 100, 100], 0.01, 0.01) }) test('[0, 10000, 10000]', async ({ page }) => { await doSnapAtDifferentScales(page, [0, 10000, 10000]) }) }) test('Sketch on face', async ({ page }) => { const u = await getUtils(page) await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XZ') |> startProfileAt([3.29, 7.86], %) |> line([2.48, 2.44], %) |> line([2.66, 1.17], %) |> line([3.75, 0.46], %) |> line([4.99, -0.46], %) |> line([3.3, -2.12], %) |> line([2.16, -3.33], %) |> line([0.85, -3.08], %) |> line([-0.18, -3.36], %) |> line([-3.86, -2.73], %) |> line([-17.67, 0.85], %) |> close(%) |> extrude(5 + 7, %)` ) }) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // wait for execution done await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() await expect( page.getByRole('button', { name: 'Start Sketch' }) ).not.toBeDisabled() await page.getByRole('button', { name: 'Start Sketch' }).click() await page.waitForTimeout(300) let previousCodeContent = await page.locator('.cm-content').innerText() await u.openAndClearDebugPanel() await u.doAndWaitForCmd( () => page.mouse.click(625, 133), 'default_camera_get_settings', true ) await page.waitForTimeout(150) const firstClickPosition = [612, 238] const secondClickPosition = [661, 242] const thirdClickPosition = [609, 267] await page.mouse.click(firstClickPosition[0], firstClickPosition[1]) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) previousCodeContent = await page.locator('.cm-content').innerText() await page.waitForTimeout(100) await page.mouse.click(secondClickPosition[0], secondClickPosition[1]) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) previousCodeContent = await page.locator('.cm-content').innerText() await page.waitForTimeout(100) await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1]) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) previousCodeContent = await page.locator('.cm-content').innerText() await page.waitForTimeout(100) await page.mouse.click(firstClickPosition[0], firstClickPosition[1]) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) previousCodeContent = await page.locator('.cm-content').innerText() await expect(page.locator('.cm-content')) .toContainText(`const part002 = startSketchOn(part001, 'seg01') |> startProfileAt([-12.94, 6.6], %) |> line([2.45, -0.2], %) |> line([-2.6, -1.25], %) |> lineTo([profileStartX(%), profileStartY(%)], %) |> close(%)`) await u.openAndClearDebugPanel() await page.getByRole('button', { name: 'Exit Sketch' }).click() await u.expectCmdLog('[data-message-type="execution-done"]') await u.updateCamPosition([1049, 239, 686]) await u.closeDebugPanel() await page.getByText('startProfileAt([-12.94, 6.6], %)').click() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await u.doAndWaitForCmd( () => page.getByRole('button', { name: 'Edit Sketch' }).click(), 'default_camera_get_settings', true ) await page.waitForTimeout(150) await page.setViewportSize({ width: 1200, height: 1200 }) await u.openAndClearDebugPanel() await u.updateCamPosition([452, -152, 1166]) await u.closeDebugPanel() await page.waitForTimeout(200) const pointToDragFirst = [787, 565] await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1]) await page.mouse.down() await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], { steps: 5, }) await page.mouse.up() await page.waitForTimeout(100) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) previousCodeContent = await page.locator('.cm-content').innerText() const result = makeTemplate`const part002 = startSketchOn(part001, 'seg01') |> startProfileAt([-12.83, 6.7], %) |> line([${[2.28, 2.35]}, -${0.07}], %) |> line([-3.05, -1.47], %) |> lineTo([profileStartX(%), profileStartY(%)], %) |> close(%)` await expect(page.locator('.cm-content')).toHaveText(result.regExp) // exit sketch await u.openAndClearDebugPanel() await page.getByRole('button', { name: 'Exit Sketch' }).click() await u.expectCmdLog('[data-message-type="execution-done"]') await page.getByText('startProfileAt([-12.94, 6.6], %)').click() await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled() await page.waitForTimeout(100) await page.getByRole('button', { name: 'Extrude' }).click() await expect(page.getByTestId('command-bar')).toBeVisible() await page.waitForTimeout(100) await page.keyboard.press('Enter') await page.waitForTimeout(100) await expect(page.getByText('Confirm Extrude')).toBeVisible() await page.keyboard.press('Enter') const result2 = result.genNext` |> extrude(${[5, 5]} + 7, %)` await expect(page.locator('.cm-content')).toHaveText(result2.regExp) }) test('Can code mod a line length', async ({ page }) => { await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> xLine(-20, %) ` ) }) const u = await getUtils(page) const PUR = 400 / 37.5 //pixeltoUnitRatio await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() // Click the line of code for line. await page.getByText(`line([0, 20], %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick() await page.waitForTimeout(100) // enter sketch again await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.waitForTimeout(500) // wait for animation const startXPx = 500 await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10) await page.keyboard.down('Shift') await page.mouse.click(834, 244) await page.keyboard.up('Shift') await page.getByRole('button', { name: 'Constrain', exact: true }).click() await page.getByRole('button', { name: 'length', exact: true }).click() await page.getByText('Add constraining value').click() await expect(page.locator('.cm-content')).toHaveText( `const length001 = 20const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)` ) // Make sure we didn't pop out of sketch mode. await expect(page.getByRole('button', { name: 'Exit Sketch' })).toBeVisible() await page.waitForTimeout(500) // wait for animation // Exit sketch await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10) await page.keyboard.press('Escape') await expect( page.getByRole('button', { name: 'Exit Sketch' }) ).not.toBeVisible() }) test('Extrude from command bar selects extrude line after', async ({ page, }) => { await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> xLine(-20, %) |> close(%) ` ) }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() // Click the line of code for xLine. await page.getByText(`close(%)`).click() // TODO remove this and reinstate // await topHorzSegmentClick() await page.waitForTimeout(100) await page.getByRole('button', { name: 'Extrude' }).click() await page.waitForTimeout(100) await page.keyboard.press('Enter') await page.waitForTimeout(100) await page.keyboard.press('Enter') await page.waitForTimeout(100) await expect(page.locator('.cm-activeLine')).toHaveText( ` |> extrude(${KCL_DEFAULT_LENGTH}, %)` ) }) test.describe('Testing segment overlays', () => { test.describe('Hover over a segment should show its overlay, hovering over the input overlays should show its popover, clicking the input overlay should constrain/unconstrain it:\nfor the following segments', () => { /** * Clicks on an constrained element * @param {Page} page - The page to perform the action on * @param {Object} options - The options for the action * @param {Object} options.hoverPos - The position to hover over * @param {Object} options.constraintType - The type of constraint * @param {number} options.ang - The angle * @param {number} options.steps - The number of steps to perform */ const _clickConstrained = (page: Page) => async ({ hoverPos, constraintType, expectBeforeUnconstrained, expectAfterUnconstrained, expectFinal, ang = 45, steps = 6, }: { hoverPos: { x: number; y: number } constraintType: | 'horizontal' | 'vertical' | 'tangentialWithPrevious' | LineInputsType expectBeforeUnconstrained: string expectAfterUnconstrained: string expectFinal: string ang?: number steps?: number }) => { await expect(page.getByText('Added variable')).not.toBeVisible() const [x, y] = [ Math.cos((ang * Math.PI) / 180) * 45, Math.sin((ang * Math.PI) / 180) * 45, ] await page.mouse.move(hoverPos.x + x, hoverPos.y + y) await page.mouse.move(hoverPos.x, hoverPos.y, { steps }) await expect(page.locator('.cm-content')).toContainText( expectBeforeUnconstrained ) const constrainedLocator = page.locator( `[data-constraint-type="${constraintType}"][data-is-constrained="true"]` ) await expect(constrainedLocator).toBeVisible() await constrainedLocator.hover() await expect( await page.getByTestId('constraint-symbol-popover').count() ).toBeGreaterThan(0) await constrainedLocator.click() await expect(page.locator('.cm-content')).toContainText( expectAfterUnconstrained ) const unconstrainedLocator = page.locator( `[data-constraint-type="${constraintType}"][data-is-constrained="false"]` ) await expect(unconstrainedLocator).toBeVisible() await unconstrainedLocator.hover() await expect( await page.getByTestId('constraint-symbol-popover').count() ).toBeGreaterThan(0) await unconstrainedLocator.click() await page.getByText('Add variable').click() await expect(page.locator('.cm-content')).toContainText(expectFinal) } /** * Clicks on an unconstrained element * @param {Page} page - The page to perform the action on * @param {Object} options - The options for the action * @param {Object} options.hoverPos - The position to hover over * @param {Object} options.constraintType - The type of constraint * @param {number} options.ang - The angle * @param {number} options.steps - The number of steps to perform */ const _clickUnconstrained = (page: Page) => async ({ hoverPos, constraintType, expectBeforeUnconstrained, expectAfterUnconstrained, expectFinal, ang = 45, steps = 5, }: { hoverPos: { x: number; y: number } constraintType: | 'horizontal' | 'vertical' | 'tangentialWithPrevious' | LineInputsType expectBeforeUnconstrained: string expectAfterUnconstrained: string expectFinal: string ang?: number steps?: number }) => { const [x, y] = [ Math.cos((ang * Math.PI) / 180) * 45, Math.sin((ang * Math.PI) / 180) * 45, ] await page.mouse.move(hoverPos.x + x, hoverPos.y + y) await expect(page.getByText('Added variable')).not.toBeVisible() await page.mouse.move(hoverPos.x, hoverPos.y, { steps }) await expect(page.locator('.cm-content')).toContainText( expectBeforeUnconstrained ) const unconstrainedLocator = page.locator( `[data-constraint-type="${constraintType}"][data-is-constrained="false"]` ) await expect(unconstrainedLocator).toBeVisible() await unconstrainedLocator.hover() await expect( await page.getByTestId('constraint-symbol-popover').count() ).toBeGreaterThan(0) await unconstrainedLocator.click() await page.getByText('Add variable').click() await expect(page.locator('.cm-content')).toContainText( expectAfterUnconstrained ) await expect(page.getByText('Added variable')).not.toBeVisible() await page.mouse.move(hoverPos.x, hoverPos.y, { steps }) const constrainedLocator = page.locator( `[data-constraint-type="${constraintType}"][data-is-constrained="true"]` ) await expect(constrainedLocator).toBeVisible() await constrainedLocator.hover() await expect( await page.getByTestId('constraint-symbol-popover').count() ).toBeGreaterThan(0) await constrainedLocator.click() await expect(page.locator('.cm-content')).toContainText(expectFinal) } test('for segments [line, angledLine, lineTo, xLineTo]', async ({ page, }) => { await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XZ') |> startProfileAt([0, 0], %) |> line([0.5, -14 + 0], %) |> angledLine({ angle: 3 + 0, length: 32 + 0 }, %) |> lineTo([33, 11.5 + 0], %) |> xLineTo(9 - 5, %) |> yLineTo(-10.77, %, 'a') |> xLine(26.04, %) |> yLine(21.14 + 0, %) |> angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %) |> angledLineOfYLength({ angle: -91, length: 19 + 0 }, %) |> angledLineToX({ angle: 3 + 0, to: 26 }, %) |> angledLineToY({ angle: 89, to: 9.14 + 0 }, %) |> angledLineThatIntersects({ angle: 4.14, intersectTag: 'a', offset: 9 }, %) |> tangentialArcTo([3.14 + 13, 3.14], %) ` ) }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // wait for execution done await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() await u.openAndClearDebugPanel() await u.sendCustomCmd({ type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_look_at', vantage: { x: 0, y: -1250, z: 580 }, center: { x: 0, y: 0, z: 0 }, up: { x: 0, y: 0, z: 1 }, }, }) await page.waitForTimeout(100) await u.sendCustomCmd({ type: 'modeling_cmd_req', cmd_id: uuidv4(), cmd: { type: 'default_camera_get_settings', }, }) await page.waitForTimeout(100) await page.getByText('xLineTo(9 - 5, %)').click() await page.waitForTimeout(100) await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.waitForTimeout(500) await expect(page.getByTestId('segment-overlay')).toHaveCount(13) const clickUnconstrained = _clickUnconstrained(page) const clickConstrained = _clickConstrained(page) const line = await u.getBoundingBox(`[data-overlay-index="${0}"]`) console.log('line1') await clickConstrained({ hoverPos: { x: line.x, y: line.y - 10 }, constraintType: 'yRelative', expectBeforeUnconstrained: '|> line([0.5, -14 + 0], %)', expectAfterUnconstrained: '|> line([0.5, -14], %)', expectFinal: '|> line([0.5, yRel001], %)', ang: 135, }) console.log('line2') await clickUnconstrained({ hoverPos: { x: line.x, y: line.y - 10 }, constraintType: 'xRelative', expectBeforeUnconstrained: '|> line([0.5, yRel001], %)', expectAfterUnconstrained: 'line([xRel001, yRel001], %)', expectFinal: '|> line([0.5, yRel001], %)', ang: -45, }) const angledLine = await u.getBoundingBox(`[data-overlay-index="1"]`) console.log('angledLine1') await clickConstrained({ hoverPos: { x: angledLine.x - 10, y: angledLine.y }, constraintType: 'angle', expectBeforeUnconstrained: 'angledLine({ angle: 3 + 0, length: 32 + 0 }, %)', expectAfterUnconstrained: 'angledLine({ angle: 3, length: 32 + 0 }, %)', expectFinal: 'angledLine({ angle: angle001, length: 32 + 0 }, %)', }) console.log('angledLine2') await clickConstrained({ hoverPos: { x: angledLine.x - 10, y: angledLine.y }, constraintType: 'length', expectBeforeUnconstrained: 'angledLine({ angle: angle001, length: 32 + 0 }, %)', expectAfterUnconstrained: 'angledLine({ angle: angle001, length: 32 }, %)', expectFinal: 'angledLine({ angle: angle001, length: len001 }, %)', }) await page.mouse.move(700, 250) for (let i = 0; i < 5; i++) { await page.mouse.wheel(0, 100) await page.waitForTimeout(25) } await page.waitForTimeout(200) const lineTo = await u.getBoundingBox(`[data-overlay-index="2"]`) console.log('lineTo1') await clickConstrained({ hoverPos: { x: lineTo.x, y: lineTo.y + 21 }, constraintType: 'yAbsolute', expectBeforeUnconstrained: 'lineTo([33, 11.5 + 0], %)', expectAfterUnconstrained: 'lineTo([33, 11.5], %)', expectFinal: 'lineTo([33, yAbs001], %)', steps: 8, ang: 55, }) console.log('lineTo2') await clickUnconstrained({ hoverPos: { x: lineTo.x, y: lineTo.y + 25 }, constraintType: 'xAbsolute', expectBeforeUnconstrained: 'lineTo([33, yAbs001], %)', expectAfterUnconstrained: 'lineTo([xAbs001, yAbs001], %)', expectFinal: 'lineTo([33, yAbs001], %)', steps: 8, }) const xLineTo = await u.getBoundingBox(`[data-overlay-index="3"]`) console.log('xlineTo1') await clickConstrained({ hoverPos: { x: xLineTo.x + 15, y: xLineTo.y }, constraintType: 'xAbsolute', expectBeforeUnconstrained: 'xLineTo(9 - 5, %)', expectAfterUnconstrained: 'xLineTo(4, %)', expectFinal: 'xLineTo(xAbs002, %)', ang: -45, steps: 8, }) }) test('for segments [yLineTo, xLine]', async ({ page }) => { await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const yRel001 = -14 const xRel001 = 0.5 const angle001 = 3 const len001 = 32 const yAbs001 = 11.5 const xAbs001 = 33 const xAbs002 = 4 const part001 = startSketchOn('XZ') |> startProfileAt([0, 0], %) |> line([0.5, yRel001], %) |> angledLine({ angle: angle001, length: len001 }, %) |> lineTo([33, yAbs001], %) |> xLineTo(xAbs002, %) |> yLineTo(-10.77, %, 'a') |> xLine(26.04, %) |> yLine(21.14 + 0, %) |> angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %) ` ) }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // wait for execution done await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() await page.getByText('xLine(26.04, %)').click() await page.waitForTimeout(100) await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.waitForTimeout(500) await expect(page.getByTestId('segment-overlay')).toHaveCount(8) const clickUnconstrained = _clickUnconstrained(page) await page.mouse.move(700, 250) for (let i = 0; i < 7; i++) { await page.mouse.wheel(0, 100) await page.waitForTimeout(25) } await page.waitForTimeout(300) const yLineTo = await u.getBoundingBox(`[data-overlay-index="4"]`) console.log('ylineTo1') await clickUnconstrained({ hoverPos: { x: yLineTo.x, y: yLineTo.y - 30 }, constraintType: 'yAbsolute', expectBeforeUnconstrained: "yLineTo(-10.77, %, 'a')", expectAfterUnconstrained: "yLineTo(yAbs002, %, 'a')", expectFinal: "yLineTo(-10.77, %, 'a')", }) const xLine = await u.getBoundingBox(`[data-overlay-index="5"]`) console.log('xline') await clickUnconstrained({ hoverPos: { x: xLine.x - 25, y: xLine.y }, constraintType: 'xRelative', expectBeforeUnconstrained: 'xLine(26.04, %)', expectAfterUnconstrained: 'xLine(xRel002, %)', expectFinal: 'xLine(26.04, %)', steps: 10, ang: 50, }) }) test('for segments [yLine, angledLineOfXLength, angledLineOfYLength]', async ({ page, }) => { await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XZ') |> startProfileAt([0, 0], %) |> line([0.5, -14 + 0], %) |> angledLine({ angle: 3 + 0, length: 32 + 0 }, %) |> lineTo([33, 11.5 + 0], %) |> xLineTo(9 - 5, %) |> yLineTo(-10.77, %, 'a') |> xLine(26.04, %) |> yLine(21.14 + 0, %) |> angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %) |> angledLineOfYLength({ angle: -91, length: 19 + 0 }, %) |> angledLineToX({ angle: 3 + 0, to: 26 }, %) |> angledLineToY({ angle: 89, to: 9.14 + 0 }, %) |> angledLineThatIntersects({ angle: 4.14, intersectTag: 'a', offset: 9 }, %) |> tangentialArcTo([3.14 + 13, 3.14], %) ` ) }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // wait for execution done await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() await page.getByText('xLineTo(9 - 5, %)').click() await page.waitForTimeout(100) await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.waitForTimeout(500) await expect(page.getByTestId('segment-overlay')).toHaveCount(13) const clickUnconstrained = _clickUnconstrained(page) const clickConstrained = _clickConstrained(page) const yLine = await u.getBoundingBox(`[data-overlay-index="6"]`) console.log('yline1') await clickConstrained({ hoverPos: { x: yLine.x, y: yLine.y + 20 }, constraintType: 'yRelative', expectBeforeUnconstrained: 'yLine(21.14 + 0, %)', expectAfterUnconstrained: 'yLine(21.14, %)', expectFinal: 'yLine(yRel001, %)', }) const angledLineOfXLength = await u.getBoundingBox( `[data-overlay-index="7"]` ) console.log('angledLineOfXLength1') await clickConstrained({ hoverPos: { x: angledLineOfXLength.x + 20, y: angledLineOfXLength.y }, constraintType: 'angle', expectBeforeUnconstrained: 'angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %)', expectAfterUnconstrained: 'angledLineOfXLength({ angle: -179, length: 23.14 }, %)', expectFinal: 'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)', }) console.log('angledLineOfXLength2') await clickUnconstrained({ hoverPos: { x: angledLineOfXLength.x + 25, y: angledLineOfXLength.y }, constraintType: 'xRelative', expectBeforeUnconstrained: 'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)', expectAfterUnconstrained: 'angledLineOfXLength({ angle: angle001, length: xRel001 }, %)', expectFinal: 'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)', steps: 7, }) const angledLineOfYLength = await u.getBoundingBox( `[data-overlay-index="8"]` ) console.log('angledLineOfYLength1') await clickUnconstrained({ hoverPos: { x: angledLineOfYLength.x, y: angledLineOfYLength.y - 20 }, constraintType: 'angle', expectBeforeUnconstrained: 'angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)', expectAfterUnconstrained: 'angledLineOfYLength({ angle: angle002, length: 19 + 0 }, %)', expectFinal: 'angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)', ang: 135, steps: 6, }) console.log('angledLineOfYLength2') await clickConstrained({ hoverPos: { x: angledLineOfYLength.x, y: angledLineOfYLength.y - 20 }, constraintType: 'yRelative', expectBeforeUnconstrained: 'angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)', expectAfterUnconstrained: 'angledLineOfYLength({ angle: -91, length: 19 }, %)', expectFinal: 'angledLineOfYLength({ angle: -91, length: yRel002 }, %)', ang: -45, steps: 7, }) }) test('for segments [angledLineToX, angledLineToY, angledLineThatIntersects]', async ({ page, }) => { test.skip(process.platform !== 'darwin', 'too flakey on ubuntu') await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XZ') |> startProfileAt([0, 0], %) |> line([0.5, -14 + 0], %) |> angledLine({ angle: 3 + 0, length: 32 + 0 }, %) |> lineTo([33, 11.5 + 0], %) |> xLineTo(9 - 5, %) |> yLineTo(-10.77, %, 'a') |> xLine(26.04, %) |> yLine(21.14 + 0, %) |> angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %) |> angledLineOfYLength({ angle: -91, length: 19 + 0 }, %) |> angledLineToX({ angle: 3 + 0, to: 26 }, %) |> angledLineToY({ angle: 89, to: 9.14 + 0 }, %) |> angledLineThatIntersects({ angle: 4.14, intersectTag: 'a', offset: 9 }, %) |> tangentialArcTo([3.14 + 13, 1.14], %) ` ) }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // wait for execution done await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() await page.getByText('xLineTo(9 - 5, %)').click() await page.waitForTimeout(100) await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.waitForTimeout(500) await expect(page.getByTestId('segment-overlay')).toHaveCount(13) const clickUnconstrained = _clickUnconstrained(page) const clickConstrained = _clickConstrained(page) const angledLineToX = await u.getBoundingBox(`[data-overlay-index="9"]`) console.log('angledLineToX') await clickConstrained({ hoverPos: { x: angledLineToX.x - 20, y: angledLineToX.y }, constraintType: 'angle', expectBeforeUnconstrained: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)', expectAfterUnconstrained: 'angledLineToX({ angle: 3, to: 26 }, %)', expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)', }) console.log('angledLineToX2') await clickUnconstrained({ hoverPos: { x: angledLineToX.x - 20, y: angledLineToX.y }, constraintType: 'xAbsolute', expectBeforeUnconstrained: 'angledLineToX({ angle: angle001, to: 26 }, %)', expectAfterUnconstrained: 'angledLineToX({ angle: angle001, to: xAbs001 }, %)', expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)', }) const angledLineToY = await u.getBoundingBox(`[data-overlay-index="10"]`) console.log('angledLineToY') await clickUnconstrained({ hoverPos: { x: angledLineToY.x, y: angledLineToY.y + 20 }, constraintType: 'angle', expectBeforeUnconstrained: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)', expectAfterUnconstrained: 'angledLineToY({ angle: angle002, to: 9.14 + 0 }, %)', expectFinal: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)', steps: process.platform === 'darwin' ? 8 : 9, ang: 135, }) console.log('angledLineToY2') await clickConstrained({ hoverPos: { x: angledLineToY.x, y: angledLineToY.y + 20 }, constraintType: 'yAbsolute', expectBeforeUnconstrained: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)', expectAfterUnconstrained: 'angledLineToY({ angle: 89, to: 9.14 }, %)', expectFinal: 'angledLineToY({ angle: 89, to: yAbs001 }, %)', ang: 135, }) const angledLineThatIntersects = await u.getBoundingBox( `[data-overlay-index="11"]` ) console.log('angledLineThatIntersects') await clickUnconstrained({ hoverPos: { x: angledLineThatIntersects.x + 20, y: angledLineThatIntersects.y, }, constraintType: 'angle', expectBeforeUnconstrained: `angledLineThatIntersects({ angle: 4.14, intersectTag: 'a', offset: 9 }, %)`, expectAfterUnconstrained: `angledLineThatIntersects({ angle: angle003, intersectTag: 'a', offset: 9 }, %)`, expectFinal: `angledLineThatIntersects({ angle: -176, offset: 9, intersectTag: 'a' }, %)`, ang: -45, }) console.log('angledLineThatIntersects2') await clickUnconstrained({ hoverPos: { x: angledLineThatIntersects.x + 20, y: angledLineThatIntersects.y, }, constraintType: 'intersectionOffset', expectBeforeUnconstrained: `angledLineThatIntersects({ angle: -176, offset: 9, intersectTag: 'a' }, %)`, expectAfterUnconstrained: `angledLineThatIntersects({ angle: -176, offset: perpDist001, intersectTag: 'a' }, %)`, expectFinal: `angledLineThatIntersects({ angle: -176, offset: 9, intersectTag: 'a' }, %)`, ang: -25, }) }) test('for segment [tangentialArcTo]', async ({ page }) => { await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XZ') |> startProfileAt([0, 0], %) |> line([0.5, -14 + 0], %) |> angledLine({ angle: 3 + 0, length: 32 + 0 }, %) |> lineTo([33, 11.5 + 0], %) |> xLineTo(9 - 5, %) |> yLineTo(-10.77, %, 'a') |> xLine(26.04, %) |> yLine(21.14 + 0, %) |> angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %) |> angledLineOfYLength({ angle: -91, length: 19 + 0 }, %) |> angledLineToX({ angle: 3 + 0, to: 26 }, %) |> angledLineToY({ angle: 89, to: 9.14 + 0 }, %) |> angledLineThatIntersects({ angle: 4.14, intersectTag: 'a', offset: 9 }, %) |> tangentialArcTo([3.14 + 13, -3.14], %) ` ) }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // wait for execution done await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() await page.getByText('xLineTo(9 - 5, %)').click() await page.waitForTimeout(100) await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.waitForTimeout(500) await expect(page.getByTestId('segment-overlay')).toHaveCount(13) const clickUnconstrained = _clickUnconstrained(page) const clickConstrained = _clickConstrained(page) const tangentialArcTo = await u.getBoundingBox( `[data-overlay-index="12"]` ) console.log('tangentialArcTo') await clickConstrained({ hoverPos: { x: tangentialArcTo.x - 10, y: tangentialArcTo.y + 20 }, constraintType: 'xAbsolute', expectBeforeUnconstrained: 'tangentialArcTo([3.14 + 13, -3.14], %)', expectAfterUnconstrained: 'tangentialArcTo([16.14, -3.14], %)', expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)', ang: -45, steps: 6, }) console.log('tangentialArcTo2') await clickUnconstrained({ hoverPos: { x: tangentialArcTo.x - 10, y: tangentialArcTo.y + 20 }, constraintType: 'yAbsolute', expectBeforeUnconstrained: 'tangentialArcTo([xAbs001, -3.14], %)', expectAfterUnconstrained: 'tangentialArcTo([xAbs001, yAbs001], %)', expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)', ang: -135, steps: 10, }) }) }) test.describe('Testing deleting a segment', () => { const _deleteSegmentSequence = (page: Page) => async ({ hoverPos, codeToBeDeleted, stdLibFnName, ang = 45, steps = 6, }: { hoverPos: { x: number; y: number } codeToBeDeleted: string stdLibFnName: string ang?: number steps?: number }) => { await expect(page.getByText('Added variable')).not.toBeVisible() const [x, y] = [ Math.cos((ang * Math.PI) / 180) * 45, Math.sin((ang * Math.PI) / 180) * 45, ] await page.mouse.move(hoverPos.x + x, hoverPos.y + y) await page.mouse.move(hoverPos.x, hoverPos.y, { steps }) await expect(page.locator('.cm-content')).toContainText(codeToBeDeleted) await page.locator(`[data-stdlib-fn-name="${stdLibFnName}"]`).click() await page.getByText('Delete Segment').click() await expect(page.locator('.cm-content')).not.toContainText( codeToBeDeleted ) } test('all segment types', async ({ page }) => { await page.addInitScript(async () => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XZ') |> startProfileAt([0, 0], %) |> line([0.5, -14 + 0], %) |> angledLine({ angle: 3 + 0, length: 32 + 0 }, %) |> lineTo([33, 11.5 + 0], %) |> xLineTo(9 - 5, %) |> yLineTo(-10.77, %, 'a') |> xLine(26.04, %) |> yLine(21.14 + 0, %) |> angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %) |> angledLineOfYLength({ angle: -91, length: 19 + 0 }, %) |> angledLineToX({ angle: 3 + 0, to: 26 }, %) |> angledLineToY({ angle: 89, to: 9.14 + 0 }, %) |> angledLineThatIntersects({ angle: 4.14, intersectTag: 'a', offset: 9 }, %) |> tangentialArcTo([3.14 + 13, 1.14], %) ` ) }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() // wait for execution done await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() await page.getByText('xLineTo(9 - 5, %)').click() await page.waitForTimeout(100) await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.waitForTimeout(500) await expect(page.getByTestId('segment-overlay')).toHaveCount(13) const deleteSegmentSequence = _deleteSegmentSequence(page) let segmentToDelete const getOverlayByIndex = (index: number) => u.getBoundingBox(`[data-overlay-index="${index}"]`) segmentToDelete = await getOverlayByIndex(12) await deleteSegmentSequence({ hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y + 20 }, codeToBeDeleted: 'tangentialArcTo([3.14 + 13, 1.14], %)', stdLibFnName: 'tangentialArcTo', ang: -45, steps: 6, }) segmentToDelete = await getOverlayByIndex(0) await deleteSegmentSequence({ hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 20 }, codeToBeDeleted: 'line([0.5, -14 + 0], %)', stdLibFnName: 'line', ang: -45, }) segmentToDelete = await getOverlayByIndex(0) await deleteSegmentSequence({ hoverPos: { x: segmentToDelete.x - 20, y: segmentToDelete.y }, codeToBeDeleted: 'angledLine({ angle: 3 + 0, length: 32 + 0 }, %)', stdLibFnName: 'angledLine', ang: 135, }) await page.waitForTimeout(200) segmentToDelete = await getOverlayByIndex(9) await deleteSegmentSequence({ hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y }, codeToBeDeleted: `angledLineThatIntersects({ angle: 4.14, intersectTag: 'a', offset: 9 }, %)`, stdLibFnName: 'angledLineThatIntersects', ang: -45, steps: 7, }) segmentToDelete = await getOverlayByIndex(8) await deleteSegmentSequence({ hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y }, codeToBeDeleted: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)', stdLibFnName: 'angledLineToY', }) segmentToDelete = await getOverlayByIndex(7) await deleteSegmentSequence({ hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y }, codeToBeDeleted: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)', stdLibFnName: 'angledLineToX', }) segmentToDelete = await getOverlayByIndex(6) await deleteSegmentSequence({ hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 }, codeToBeDeleted: 'angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)', stdLibFnName: 'angledLineOfYLength', }) segmentToDelete = await getOverlayByIndex(5) await deleteSegmentSequence({ hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y }, codeToBeDeleted: 'angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %)', stdLibFnName: 'angledLineOfXLength', }) segmentToDelete = await getOverlayByIndex(4) await deleteSegmentSequence({ hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y + 10 }, codeToBeDeleted: 'yLine(21.14 + 0, %)', stdLibFnName: 'yLine', }) segmentToDelete = await getOverlayByIndex(3) await deleteSegmentSequence({ hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y }, codeToBeDeleted: 'xLine(26.04, %)', stdLibFnName: 'xLine', }) segmentToDelete = await getOverlayByIndex(2) await deleteSegmentSequence({ hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 }, codeToBeDeleted: "yLineTo(-10.77, %, 'a')", stdLibFnName: 'yLineTo', }) segmentToDelete = await getOverlayByIndex(1) await deleteSegmentSequence({ hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y }, codeToBeDeleted: 'xLineTo(9 - 5, %)', stdLibFnName: 'xLineTo', }) for (let i = 0; i < 15; i++) { await page.mouse.wheel(0, 100) await page.waitForTimeout(25) } await page.waitForTimeout(200) segmentToDelete = await getOverlayByIndex(0) const hoverPos = { x: segmentToDelete.x - 10, y: segmentToDelete.y + 10 } await expect(page.getByText('Added variable')).not.toBeVisible() const [x, y] = [ Math.cos((45 * Math.PI) / 180) * 45, Math.sin((45 * Math.PI) / 180) * 45, ] await page.mouse.move(hoverPos.x + x, hoverPos.y + y) await page.mouse.move(hoverPos.x, hoverPos.y, { steps: 5 }) const codeToBeDeleted = 'lineTo([33, 11.5 + 0], %)' await expect(page.locator('.cm-content')).toContainText(codeToBeDeleted) await page.getByTestId('overlay-menu').click() await page.getByText('Delete Segment').click() await expect(page.locator('.cm-content')).not.toContainText( codeToBeDeleted ) }) }) test.describe('Testing delete with dependent segments', () => { const cases = [ "line([22, 2], %, 'seg01')", "angledLine([5, 23.03], %, 'seg01')", "xLine(23, %, 'seg01')", "yLine(-8, %, 'seg01')", "xLineTo(30, %, 'seg01')", "yLineTo(-4, %, 'seg01')", "angledLineOfXLength([3, 30], %, 'seg01')", "angledLineOfXLength({ angle: 3, length: 30 }, %, 'seg01')", "angledLineOfYLength([3, 1.5], %, 'seg01')", "angledLineOfYLength({ angle: 3, length: 1.5 }, %, 'seg01')", "angledLineToX([3, 30], %, 'seg01')", "angledLineToX({ angle: 3, to: 30 }, %, 'seg01')", "angledLineToY([3, 7], %, 'seg01')", "angledLineToY({ angle: 3, to: 7 }, %, 'seg01')", ] for (const doesHaveTagOutsideSketch of [true, false]) { for (const lineOfInterest of cases) { const isObj = lineOfInterest.includes('{ angle: 3,') test(`${lineOfInterest.split('(')[0]}${isObj ? '-[obj-input]' : ''}${ doesHaveTagOutsideSketch ? '-[tagOutsideSketch]' : '' }`, async ({ page }) => { await page.addInitScript( async ({ lineToBeDeleted, extraLine }) => { localStorage.setItem( 'persistCode', `const part001 = startSketchOn('XZ') |> startProfileAt([5, 6], %) |> ${lineToBeDeleted} |> line([-10, -15], %) |> angledLine([-176, segLen('seg01', %)], %) ${extraLine ? "const myVar = segLen('seg01', part001)" : ''}` ) }, { lineToBeDeleted: lineOfInterest, extraLine: doesHaveTagOutsideSketch, } ) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() await page.waitForTimeout(300) await page.getByText(lineOfInterest).click() await page.waitForTimeout(100) await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.waitForTimeout(500) await expect(page.getByTestId('segment-overlay')).toHaveCount(3) const segmentToDelete = await u.getBoundingBox( `[data-overlay-index="0"]` ) const isYLine = lineOfInterest.toLowerCase().includes('yline') const hoverPos = { x: segmentToDelete.x + (isYLine ? 0 : -20), y: segmentToDelete.y + (isYLine ? -20 : 0), } await expect(page.getByText('Added variable')).not.toBeVisible() const ang = isYLine ? 45 : -45 const [x, y] = [ Math.cos((ang * Math.PI) / 180) * 45, Math.sin((ang * Math.PI) / 180) * 45, ] await page.mouse.move(hoverPos.x + x, hoverPos.y + y) await page.mouse.move(hoverPos.x, hoverPos.y, { steps: 5 }) await expect(page.locator('.cm-content')).toContainText( lineOfInterest ) await page.getByTestId('overlay-menu').click() await page.getByText('Delete Segment').click() await page.getByText('Cancel').click() await page.mouse.move(hoverPos.x + x, hoverPos.y + y) await page.mouse.move(hoverPos.x, hoverPos.y, { steps: 5 }) await expect(page.locator('.cm-content')).toContainText( lineOfInterest ) await page.getByTestId('overlay-menu').click() await page.getByText('Delete Segment').click() await page.getByText('Continue and unconstrain').last().click() if (doesHaveTagOutsideSketch) { // eslint-disable-next-line jest/no-conditional-expect await expect( page.getByText( 'Segment tag used outside of current Sketch. Could not delete.' ) ).toBeTruthy() // eslint-disable-next-line jest/no-conditional-expect await expect(page.locator('.cm-content')).toContainText( lineOfInterest ) } else { // eslint-disable-next-line jest/no-conditional-expect await expect(page.locator('.cm-content')).not.toContainText( lineOfInterest ) // eslint-disable-next-line jest/no-conditional-expect await expect(page.locator('.cm-content')).not.toContainText('seg01') } }) } } }) }) test('First escape in tool pops you out of tool, second exits sketch mode', async ({ page, }) => { // Wait for the app to be ready for use const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() const lineButton = page.getByRole('button', { name: 'Line' }) const arcButton = page.getByRole('button', { name: 'Tangential Arc' }) // Test these hotkeys perform actions when // focus is on the canvas await page.mouse.move(600, 250) await page.mouse.click(600, 250) // Start a sketch await page.keyboard.press('s') await page.mouse.move(800, 300) await page.mouse.click(800, 300) await page.waitForTimeout(1000) await expect(lineButton).toHaveAttribute('aria-pressed', 'true') // Draw a line await page.mouse.move(700, 200, { steps: 5 }) await page.mouse.click(700, 200) await page.mouse.move(800, 250, { steps: 5 }) await page.mouse.click(800, 250) // Unequip line tool await page.keyboard.press('Escape') // Make sure we didn't pop out of sketch mode. await expect(page.getByRole('button', { name: 'Exit Sketch' })).toBeVisible() await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true') // Equip arc tool await page.keyboard.press('a') await expect(arcButton).toHaveAttribute('aria-pressed', 'true') await page.mouse.move(1000, 100, { steps: 5 }) await page.mouse.click(1000, 100) await page.keyboard.press('Escape') await page.keyboard.press('l') await expect(lineButton).toHaveAttribute('aria-pressed', 'true') // Do not close the sketch. // On close it will exit sketch mode. // Unequip line tool await page.keyboard.press('Escape') await expect(lineButton).toHaveAttribute('aria-pressed', 'false') await expect(arcButton).toHaveAttribute('aria-pressed', 'false') // Make sure we didn't pop out of sketch mode. await expect(page.getByRole('button', { name: 'Exit Sketch' })).toBeVisible() // Exit sketch await page.keyboard.press('Escape') await expect( page.getByRole('button', { name: 'Exit Sketch' }) ).not.toBeVisible() }) test('Basic default modeling and sketch hotkeys work', async ({ page }) => { // This test can run long if it takes a little too long to load // the engine. test.setTimeout(90000) // This test has a weird bug on ubuntu test.skip( process.platform === 'linux', 'weird playwright bug on ubuntu https://github.com/KittyCAD/modeling-app/issues/2444' ) // Load the app with the code pane open await page.addInitScript(async () => { localStorage.setItem( 'store', JSON.stringify({ state: { openPanes: ['code'], }, version: 0, }) ) }) // Wait for the app to be ready for use const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.closeDebugPanel() const codePane = page.getByRole('textbox').locator('div') const codePaneButton = page.getByRole('tab', { name: 'KCL Code' }) const lineButton = page.getByRole('button', { name: 'Line' }) const arcButton = page.getByRole('button', { name: 'Tangential Arc' }) const extrudeButton = page.getByRole('button', { name: 'Extrude' }) // Test that the hotkeys do nothing when // focus is on the code pane await codePane.click() await page.keyboard.press('s') await page.keyboard.press('l') await page.keyboard.press('a') await page.keyboard.press('e') await expect(page.locator('.cm-content')).toHaveText('slae') await page.keyboard.press('Meta+/') // Test these hotkeys perform actions when // focus is on the canvas await page.mouse.move(600, 250) await page.mouse.click(600, 250) // Start a sketch await page.keyboard.press('s') await page.mouse.move(800, 300) await page.mouse.click(800, 300) await page.waitForTimeout(1000) await expect(lineButton).toHaveAttribute('aria-pressed', 'true') /** * TODO: There is a bug somewhere that causes this test to fail * if you toggle the codePane closed before your trigger the * start of the sketch. * and a separate Safari-only bug that causes the test to fail * if the pane is open the entire test. The maintainer of CodeMirror * has pinpointed this to the unusual browser behavior: * https://discuss.codemirror.net/t/how-to-force-unfocus-of-the-codemirror-element-in-safari/8095/3 */ await codePaneButton.click() // Draw a line await page.mouse.move(700, 200, { steps: 5 }) await page.mouse.click(700, 200) await page.mouse.move(800, 250, { steps: 5 }) await page.mouse.click(800, 250) // Unequip line tool await page.keyboard.press('l') await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true') // Equip arc tool await page.keyboard.press('a') await expect(arcButton).toHaveAttribute('aria-pressed', 'true') await page.mouse.move(1000, 100, { steps: 5 }) await page.mouse.click(1000, 100) await page.keyboard.press('Escape') await page.keyboard.press('l') await expect(lineButton).toHaveAttribute('aria-pressed', 'true') // Close profile await page.mouse.move(700, 200, { steps: 5 }) await page.mouse.click(700, 200) // On close it will unequip the line tool. await expect(lineButton).toHaveAttribute('aria-pressed', 'false') // Exit sketch await page.keyboard.press('Escape') await expect( page.getByRole('button', { name: 'Exit Sketch' }) ).not.toBeVisible() // Extrude await page.mouse.click(750, 150) await expect(extrudeButton).not.toBeDisabled() await page.keyboard.press('e') await page.mouse.move(730, 230, { steps: 5 }) await page.mouse.click(730, 230) await page.waitForTimeout(100) await page.getByRole('button', { name: 'Continue' }).click() await page.getByRole('button', { name: 'Submit command' }).click() await codePaneButton.click() await expect(page.locator('.cm-content')).toContainText('extrude(') }) test('simulate network down and network little widget', async ({ page }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await page.goto('/') await u.waitForAuthSkipAppStart() const networkWidget = page.locator('[data-testid="network-toggle"]') await expect(networkWidget).toBeVisible() await networkWidget.hover() const networkPopover = page.locator('[data-testid="network-popover"]') await expect(networkPopover).not.toBeVisible() // Expect the network to be up await expect(page.getByText('Network Health (Connected)')).toBeVisible() // Click the network widget await networkWidget.click() // Check the modal opened. await expect(networkPopover).toBeVisible() // Click off the modal. await page.mouse.click(100, 100) await expect(networkPopover).not.toBeVisible() // Turn off the network await u.emulateNetworkConditions({ offline: true, // values of 0 remove any active throttling. crbug.com/456324#c9 latency: 0, downloadThroughput: -1, uploadThroughput: -1, }) // Expect the network to be down await expect(page.getByText('Network Health (Offline)')).toBeVisible() // Click the network widget await networkWidget.click() // Check the modal opened. await expect(networkPopover).toBeVisible() // Click off the modal. await page.mouse.click(100, 100) await expect(networkPopover).not.toBeVisible() // Turn back on the network await u.emulateNetworkConditions({ offline: false, // values of 0 remove any active throttling. crbug.com/456324#c9 latency: 0, downloadThroughput: -1, uploadThroughput: -1, }) // Expect the network to be up await expect(page.getByText('Network Health (Connected)')).toBeVisible() })