Files
modeling-app/e2e/playwright/sketch-tests.spec.ts
Frank Noirot 49b78d726a Submit selection to command on unmount of selection arg input (#7047)
* Submit selection to command on unmount of selection arg input

This fixes #7024 by saving the user's selection to the command even if
they click another argument in the command palette's header. It does no
harm to save the selection to the argument, even if it's being torn down
because the user dismissed it has no negative effect.

* Refactor to not auto-submit before selection is cleared on mount

Thanks E2E test suite

* Update failing E2E tests with new behavior, which allows skip with preselection

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-05-19 13:21:43 -04:00

3658 lines
118 KiB
TypeScript

import fs from 'node:fs/promises'
import path from 'node:path'
import type { Page } from '@playwright/test'
import { roundOff, uuidv4 } from '@src/lib/utils'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import type { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
import {
PERSIST_MODELING_CONTEXT,
TEST_COLORS,
getMovementUtils,
getUtils,
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Sketch tests', () => {
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
page,
context,
homePage,
scene,
cmdBar,
}) => {
const u = await getUtils(page)
const selectionsSnippets = {
startProfileAt1:
'|> startProfile(at = [-width / 4 + screwRadius, height / 2])',
startProfileAt2: '|> startProfile(at = [-width / 2, 0])',
startProfileAt3: '|> startProfile(at = [0, thickness])',
}
await context.addInitScript(
async ({ startProfileAt1, startProfileAt2, startProfileAt3 }: any) => {
localStorage.setItem(
'persistCode',
`
width = 20
height = 10
thickness = 5
screwRadius = 3
wireRadius = 2
wireOffset = 0.5
screwHole = startSketchOn(XY)
${startProfileAt1}
|> arc(angleStart = 0, angleEnd = 360, radius = screwRadius)
part001 = startSketchOn(XY)
${startProfileAt2}
|> xLine(length = width * .5)
|> yLine(length = height)
|> xLine(length = -width * .5)
|> close()
|> subtract2d(tool = screwHole)
|> extrude(length = thickness)
part002 = startSketchOn(-XZ)
${startProfileAt3}
|> xLine(length = width / 4)
|> tangentialArc(endAbsolute = [width / 2, 0])
|> xLine(length = -width / 4 + wireRadius)
|> yLine(length = wireOffset)
|> arc(angleStart = 0, angleEnd = 180, radius = wireRadius)
|> yLine(length = -wireOffset)
|> xLine(length = -width / 4)
|> close()
|> extrude(length = -height)
`
)
},
selectionsSnippets
)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// 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: 'Edit Sketch' })
).toBeVisible()
await page.getByText(selectionsSnippets.startProfileAt2).click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByText(selectionsSnippets.startProfileAt3).click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
})
test('Can delete most of a sketch and the line tool will still work', async ({
page,
scene,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ)
|> startProfile(at = [2.61, -4.01])
|> xLine(length = 8.73)
|> tangentialArc(endAbsolute = [8.33, -1.31])`
)
})
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 587, y: 270 }, 15)
await expect(async () => {
await page.mouse.click(700, 200)
await page.getByText('tangentialArc(endAbsolute = [8.33, -1.31])').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeEnabled({ timeout: 2000 })
await page.getByRole('button', { name: 'Edit Sketch' }).click()
}).toPass({ timeout: 40_000, intervals: [1_000] })
await page.waitForTimeout(600) // wait for animation
await page.getByText('tangentialArc(endAbsolute = [8.33, -1.31])').click()
await page.keyboard.press('End')
await page.keyboard.down('Shift')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('Home')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'line Line', exact: true }).click()
await page.waitForTimeout(500)
// click start profileAt handle to continue profile
await page.mouse.click(702, 406, { delay: 500 })
await page.waitForTimeout(100)
await page.mouse.move(800, 150)
await expect(async () => {
// click to add segment
await page.mouse.click(700, 200)
await expect
.poll(u.normalisedEditorCode, { timeout: 1000 })
.toBe(`@settings(defaultLengthUnit = in)
sketch002 = startSketchOn(XZ)
sketch001 = startProfile(sketch002, at = [12.34, -12.34])
|> yLine(length = 12.34)
`)
}).toPass({ timeout: 5_000, intervals: [1_000] })
})
test('Can exit selection of face', async ({ page, homePage }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem('persistCode', ``)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(page.getByText('select a plane or face')).toBeVisible()
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
})
test('Can edit segments by dragging their handles', () => {
const doEditSegmentsByDraggingHandle = async (
page: Page,
homePage: HomePageFixture,
openPanes: string[],
scene: SceneFixture,
toolbar: ToolbarFixture,
cmdBar: CmdBarFixture
) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn(XZ)
|> startProfile(at = [4.61, -14.01])
|> line(end = [12.73, -0.09])
|> tangentialArc(endAbsolute = [24.95, -5.38])
|> arc(interiorAbsolute = [20.18, -1.7], endAbsolute = [11.82, -1.16])
|> arc(angleStart = -89.36, angleEnd = 135.81, radius = 5.92)
|> close()`
)
})
const u = await getUtils(page)
await homePage.goToModelingScene()
await scene.settled(cmdBar)
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)
await u.closeDebugPanel()
// If we have the code pane open, we should see the code.
if (openPanes.includes('code')) {
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn(XZ)
|> startProfile(at = [4.61, -14.01])
|> line(end = [12.73, -0.09])
|> tangentialArc(endAbsolute = [24.95, -5.38])
|> arc(interiorAbsolute = [20.18, -1.7], endAbsolute = [11.82, -1.16])
|> arc(angleStart = -89.36, angleEnd = 135.81, radius = 5.92)
|> close()
`)
} else {
// Ensure we don't see the code.
await expect(u.codeLocator).not.toBeVisible()
}
const startPX = [665, 458]
const dragPX = 30
let prevContent = ''
if (openPanes.includes('code')) {
await page.getByText('startProfile(at = [4.61, -14.01])').click()
} else {
// Wait for the render.
await page.waitForTimeout(1000)
// Select the sketch
await page.mouse.click(700, 370)
}
await toolbar.editSketch()
if (openPanes.includes('code')) {
prevContent = await page.locator('.cm-content').innerText()
}
const step5 = { steps: 5 }
await expect(page.getByTestId('segment-overlay')).toHaveCount(5)
// drag startProfileAt 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()
if (openPanes.includes('code')) {
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)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
}
// drag tangentialArc 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)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
}
// drag arcTo interiorAbsolute handle (three point arc)
const arcToHandle = await u.getBoundingBox('[data-overlay-index="2"]')
await page.mouse.move(arcToHandle.x, arcToHandle.y - 5)
await page.mouse.down()
await page.mouse.move(
arcToHandle.x - dragPX,
arcToHandle.y + dragPX,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
}
// drag arcTo end handle (three point arc)
const arcToEndHandle = await u.getBoundingBox('[data-overlay-index="3"]')
await page.mouse.move(arcToEndHandle.x, arcToEndHandle.y - 5)
await page.mouse.down()
await page.mouse.move(
arcToEndHandle.x - dragPX,
arcToEndHandle.y + dragPX,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
}
// drag arc radius handle
const arcRadiusHandle = await u.getBoundingBox('[data-overlay-index="4"]')
await page.mouse.move(arcRadiusHandle.x, arcRadiusHandle.y - 5)
await page.mouse.down()
await page.mouse.move(
arcRadiusHandle.x - dragPX,
arcRadiusHandle.y + dragPX,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
}
// drag arc center handle (we'll have to hardcode the position because it doesn't have a overlay near the handle)
const arcCenterHandle = { x: 745, y: 214 }
await page.mouse.move(arcCenterHandle.x, arcCenterHandle.y - 5)
await page.mouse.down()
await page.mouse.move(
arcCenterHandle.x - dragPX,
arcCenterHandle.y + dragPX,
step5
)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
}
// Open the code pane
await u.openKclCodePanel()
// expect the code to have changed
await expect(
page.locator('.cm-content')
).toHaveText(`sketch001 = startSketchOn(XZ)
|> startProfile(at = [6.44, -12.07])
|> line(end = [14.72, 1.97])
|> tangentialArc(endAbsolute = [26.92, -3.32])
|> arc(interiorAbsolute = [18.11, -3.73], endAbsolute = [9.77, -3.19])
|> arc(angleStart = -58.29, angleEnd = 161.17, radius = 3.75)
|> close()
`)
}
test('code pane open at start-handles', async ({
page,
homePage,
scene,
toolbar,
cmdBar,
}) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
'store',
JSON.stringify({
state: {
openPanes: ['code'],
},
version: 0,
})
)
})
await doEditSegmentsByDraggingHandle(
page,
homePage,
['code'],
scene,
toolbar,
cmdBar
)
})
test('code pane closed at start-handles', async ({
page,
homePage,
scene,
toolbar,
cmdBar,
}) => {
// Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem(
persistModelingContext,
JSON.stringify({ openPanes: [] })
)
}, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(
page,
homePage,
[],
scene,
toolbar,
cmdBar
)
})
})
test('Can edit a circle center and radius by dragging its handles', async ({
page,
editor,
homePage,
scene,
cmdBar,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit=in)
sketch001 = startSketchOn(XZ)
|> circle(center = [4.61, -5.01], radius = 8)`
)
})
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
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 = [667, 325]
const dragPX = 40
await page.getByText('circle(center = [4.61, -5.01], radius = 8)').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()
await expect(page.getByTestId('segment-overlay')).toHaveCount(1)
await test.step('drag circle center handle', async () => {
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0], y: startPX[1] },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] - dragPX },
})
await page.waitForTimeout(100)
await editor.expectEditor.not.toContain(prevContent)
prevContent = await page.locator('.cm-content').innerText()
})
await test.step('drag circle radius handle', async () => {
await page.waitForTimeout(100)
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.waitForTimeout(100)
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX * 2, y: lineEnd.y + dragPX },
})
await editor.expectEditor.not.toContain(prevContent)
prevContent = await page.locator('.cm-content').innerText()
})
// expect the code to have changed
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
|> circle(center = [7.26, -2.37], radius = 11.44)`,
{ shouldNormalise: true }
)
})
test('Can edit a sketch that has been extruded in the same pipe', async ({
page,
homePage,
editor,
toolbar,
scene,
cmdBar,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit=in)
sketch001 = startSketchOn(XZ)
|> startProfile(at = [4.61, -10.01])
|> line(end = [12.73, -0.09])
|> tangentialArc(endAbsolute = [24.95, -0.38])
|> close()
|> extrude(length = 5)`
)
})
await homePage.goToModelingScene()
await toolbar.waitForFeatureTreeToBeBuilt()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 10_000 })
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, 397]
const dragPX = 40
await page.getByText('startProfile(at = [4.61, -10.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()
await expect(page.getByTestId('segment-overlay')).toHaveCount(3)
// drag startProfileAt handle
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0], y: startPX[1] },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
})
await page.waitForTimeout(100)
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="1"]')
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 15, y: lineEnd.y },
targetPosition: { x: lineEnd.x, y: lineEnd.y + 15 },
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag tangentialArc handle
const tangentEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: tangentEnd.x + 10, y: tangentEnd.y - 5 },
targetPosition: {
x: tangentEnd.x,
y: tangentEnd.y - 15,
},
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
|> startProfile(at = [7.12, -12.68])
|> line(end = [12.68, -1.09])
|> tangentialArc(endAbsolute = [24.95, -0.38])
|> close()
|> extrude(length = 5)`,
{ shouldNormalise: true }
)
})
test('Can edit a sketch that has been revolved in the same pipe', async ({
page,
homePage,
scene,
editor,
cmdBar,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit=in)
sketch001 = startSketchOn(XZ)
|> startProfile(at = [4.61, -14.01])
|> line(end = [12.73, -0.09])
|> tangentialArc(endAbsolute = [24.95, -5.38])
|> close()
|> revolve(axis = X)`
)
})
await homePage.goToModelingScene()
await scene.settled(cmdBar)
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('startProfile(at = [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(3)
// drag startProfileAt 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 tangentialArc 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 editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
|> startProfile(at = [8.41, -9.97])
|> line(end = [12.73, -0.09])
|> line(end = [1.99, 2.06])
|> tangentialArc(endAbsolute = [24.95, -5.38])
|> close()
|> revolve(axis = X)`,
{ shouldNormalise: true }
)
})
test('Can add multiple sketches', async ({ page, homePage }) => {
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
await page.setBodyDimensions(viewportSize)
await homePage.goToModelingScene()
await u.openDebugPanel()
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
const { toSU, toU, click00r } = getMovementUtils({ center, page })
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
)
let codeStr =
'@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XY)'
await page.mouse.click(center.x, viewportSize.height * 0.55)
await expect(u.codeLocator).toHaveText(codeStr)
await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await click00r(0, 0)
codeStr += `profile001 = startProfile(sketch001, at = ${toSU([0, 0])})`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(50, 0)
await page.waitForTimeout(100)
codeStr += ` |> xLine(length = ${toU(50, 0)[0]})`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(0, 50)
codeStr += ` |> yLine(length = ${toU(0, 50)[1]})`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(-50, 0)
codeStr += ` |> xLine(length = ${toU(-50, 0)[0]})`
await expect(u.codeLocator).toHaveText(codeStr)
// exit the sketch, reset relative clicker
await click00r(undefined, undefined)
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await page.waitForTimeout(250)
await u.clearCommandLogs()
// start a new sketch
await page.getByRole('button', { name: 'Start Sketch' }).click()
// when exiting the sketch above the camera is still looking down at XY,
// so selecting the plane again is a bit easier.
await page.mouse.click(center.x + 200, center.y + 100)
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
codeStr += 'sketch002 = startSketchOn(XY)'
await expect(u.codeLocator).toHaveText(codeStr)
await u.closeDebugPanel()
await click00r(30, 0)
codeStr += `profile002 = startProfile(sketch002, at = [2.03, 0])`
await expect(u.codeLocator).toHaveText(codeStr)
// TODO: I couldn't use `toSU` here because of some rounding error causing
// it to be off by 0.01
await click00r(30, 0)
codeStr += ` |> xLine(length = 2.04)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(0, 30)
codeStr += ` |> yLine(length = -2.03)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(-30, 0)
codeStr += ` |> xLine(length = -2.04)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(undefined, undefined)
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 u.clearCommandLogs()
})
test.describe('Snap to close works (at any scale)', () => {
const doSnapAtDifferentScales = async (
page: any,
camPos: [number, number, number],
scale = 1
) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await u.openDebugPanel()
const code = `@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(-XZ)
profile001 = startProfile(sketch001, at = [${roundOff(scale * 69.6)}, ${roundOff(
scale * 34.8
)}])
|> xLine(length = ${roundOff(scale * 139.19)})
|> yLine(length = -${roundOff(scale * 139.2)})
|> line(endAbsolute = [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()
await page.mouse.move(0, 0)
// select a plane
await page.mouse.move(700, 200, { steps: 10 })
await page.mouse.click(700, 200, { delay: 200 })
await expect(page.locator('.cm-content')).toHaveText(
`@settings(defaultLengthUnit = in)sketch001 = 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.waitForTimeout(500)
await page.mouse.move(pointA[0], pointA[1], { steps: 10 })
await page.mouse.click(pointA[0], pointA[1], { delay: 200 })
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.move(pointB[0], pointB[1], { steps: 10 })
await page.mouse.click(pointB[0], pointB[1], { delay: 200 })
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.move(pointC[0], pointC[1], { steps: 10 })
await page.mouse.click(pointC[0], pointC[1], { delay: 200 })
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, { steps: 10 })
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], {
delay: 200,
})
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await expect
.poll(async () => {
const text = await page.locator('.cm-content').innerText()
return text.replace(/\s/g, '')
})
.toBe(code.replace(/\s/g, ''))
// Assert the tool stays equipped after a profile is closed (ready for the next one)
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).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('[0, 100, 100]', async ({ page, homePage }) => {
await homePage.goToModelingScene()
await doSnapAtDifferentScales(page, [0, 100, 100], 0.01)
})
test('[0, 10000, 10000]', async ({ page, homePage }) => {
await homePage.goToModelingScene()
await doSnapAtDifferentScales(page, [0, 10000, 10000])
})
})
test('exiting a close extrude, has the extrude button enabled ready to go', async ({
page,
homePage,
cmdBar,
toolbar,
}) => {
// this was a regression https://github.com/KittyCAD/modeling-app/issues/2832
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn(XZ)
|> startProfile(at = [-0.45, 0.87])
|> line(end = [1.32, 0.38])
|> line(end = [1.02, -1.32], tag = $seg01)
|> line(end = [-1.01, -0.77])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
`
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// click profile in code
await page.getByText(`startProfile(at = [-0.45, 0.87])`).click()
await page.waitForTimeout(100)
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeEnabled(
{ timeout: 10_000 }
)
// click edit sketch
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(600) // wait for animation
// exit sketch
await page.getByRole('button', { name: 'Exit Sketch' }).click()
// expect extrude button to be enabled
await expect(toolbar.extrudeButton).not.toBeDisabled()
// click extrude
await toolbar.extrudeButton.click()
// sketch selection should already have been made.
// otherwise the cmdbar would be waiting for a selection.
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: { Profiles: '1 profile', Length: '' },
highlightedHeaderArg: 'length',
commandName: 'Extrude',
})
})
test("Existing sketch with bad code delete user's code", async ({
page,
homePage,
}) => {
// this was a regression https://github.com/KittyCAD/modeling-app/issues/2832
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn(XZ)
|> startProfile(at = [-0.45, 0.87])
|> line(end = [1.32, 0.38])
|> line(end = [1.02, -1.32], tag = $seg01)
|> line(end = [-1.01, -0.77])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 5)
`
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
// Click the end face of extrude001
await page.mouse.click(622, 355)
// The click should generate a new sketch starting on the end face of extrude001
// signified by the implicit 'END' tag for that solid.
await page.waitForTimeout(800)
await page.getByText(`END)`).click()
await page.keyboard.press('End')
await page.keyboard.press('Enter')
await page.keyboard.type(' |>', { delay: 100 })
await page.waitForTimeout(100)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
await expect((await u.codeLocator.innerText()).replace(/\s/g, '')).toBe(
`sketch001 = startSketchOn(XZ)
|> startProfile(at = [-0.45, 0.87])
|> line(end = [1.32, 0.38])
|> line(end = [1.02, -1.32], tag = $seg01)
|> line(end = [-1.01, -0.77])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 5)
sketch002 = startSketchOn(extrude001, face = END)
|>
`.replace(/\s/g, '')
)
})
// TODO: fix after electron migration is merged
test('empty-scene default-planes act as expected', async ({
page,
homePage,
scene,
cmdBar,
editor,
}) => {
/**
* Tests the following things
* 1) The the planes are there on load because the scene is empty
* 2) The planes don't changes color when hovered initially
* 3) Putting something in the scene makes the planes hidden
* 4) Removing everything from the scene shows the plans again
* 3) Once "start sketch" is click, the planes do respond to hovers
* 4) Selecting a plan works as expected, i.e. sketch mode
* 5) Reloading the scene with something already in the scene means the planes are hidden
*/
const u = await getUtils(page)
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const XYPlanePoint = { x: 774, y: 116 } as const
const unHoveredColor: [number, number, number] = [47, 47, 93]
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await u.openAndClearDebugPanel()
await u.codeLocator.fill(`sketch001 = startSketchOn(XY)
|> startProfile(at = [-10, -10])
|> line(end = [20, 0])
|> line(end = [0, 20])
|> xLine(length = -20)
`)
await u.expectCmdLog('[data-message-type="execution-done"]')
const noPlanesColor: [number, number, number] = [30, 30, 30]
expect(
await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor)
).toBeLessThan(3)
await u.clearCommandLogs()
await u.removeCurrentCode()
await u.expectCmdLog('[data-message-type="execution-done"]')
await expect
.poll(() => u.getGreatestPixDiff(XYPlanePoint, unHoveredColor), {
timeout: 5_000,
})
.toBeLessThan(8)
// click start Sketch
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y, { steps: 50 })
const hoveredColor: [number, number, number] = [93, 93, 127]
// now that we're expecting the user to select a plan, it does respond to hover
await expect
.poll(() => u.getGreatestPixDiff(XYPlanePoint, hoveredColor))
.toBeLessThan(8)
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
// wait for line button to have aria-pressed as proxy for sketch mode
await expect
.poll(async () => page.getByTestId('line').getAttribute('aria-pressed'), {
timeout: 10_000,
})
.toBe('true')
await page.waitForTimeout(200)
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50)
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [299.72, 230.82])
|> line(end = [86.12, -86.13])
`,
{ shouldNormalise: true }
)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [299.72, 230.82])
|> line(end = [86.12, -86.13])
`
)
})
await scene.settled(cmdBar)
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// expect there to be no planes on load since there's something in the scene
expect(
await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor)
).toBeLessThan(3)
})
test('Can attempt to sketch on revolved face', async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`lugHeadLength = 0.25
lugDiameter = 0.5
lugLength = 2
fn lug(origin, length, diameter, plane) {
lugSketch = startSketchOn(plane)
|> startProfile(at = [origin[0] + lugDiameter / 2, origin[1]])
|> angledLine(angle = 60, lengthY = lugHeadLength)
|> xLine(endAbsolute = 0 + .001)
|> yLine(endAbsolute = 0)
|> close()
|> revolve(axis = Y)
return lugSketch
}
lug(origin = [0, 0], length = 10, diameter = .5, plane = XY)`
)
})
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
/***
* Test Plan
* Start the sketch mode
* Click the middle of the screen which should click the top face that is revolved
* Wait till you see the line tool be enabled
* Wait till you see the exit sketch enabled
*
* This is supposed to test that you are allowed to go into sketch mode to sketch on a revolved face
*/
await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect(async () => {
await page.mouse.click(600, 250)
await page.waitForTimeout(1000)
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).toHaveAttribute('aria-pressed', 'true')
}).toPass({ timeout: 40_000, intervals: [1_000] })
})
test('sketch on face of a boolean works', async ({
page,
homePage,
scene,
cmdBar,
toolbar,
editor,
}) => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = mm)
myVar = 50
sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [myVar, 43.9], radius = 41.05)
extrude001 = extrude(profile001, length = 200)
|> translate(x = 3.14, y = 3.14, z = -50.154)
sketch002 = startSketchOn(XY)
profile002 = startProfile(sketch002, at = [72.2, -52.05])
|> angledLine(angle = 0, length = 181.26, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 21.54)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $mySeg)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
extrude002 = extrude(profile002, length = 151)
solid001 = subtract([extrude001], tools = [extrude002])
`
)
})
const [selectChamferFaceClk] = scene.makeMouseHelpers(705, 234)
const [circleCenterClk] = scene.makeMouseHelpers(700, 272)
const [circleRadiusClk] = scene.makeMouseHelpers(694, 264)
await test.step('Setup', async () => {
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await scene.moveCameraTo(
{ x: 180, y: -75, z: 116 },
{ x: 67, y: -114, z: -15 }
)
await toolbar.waitForFeatureTreeToBeBuilt()
})
await test.step('sketch on chamfer face that is part of a boolean', async () => {
await toolbar.startSketchPlaneSelection()
await selectChamferFaceClk()
await expect
.poll(async () => {
const lineBtn = page.getByRole('button', { name: 'line Line' })
return lineBtn.getAttribute('aria-pressed')
})
.toBe('true')
await editor.expectEditor.toContain(
'startSketchOn(solid001, face = seg01)'
)
})
await test.step('verify sketching still works', async () => {
await toolbar.circleBtn.click()
await expect
.poll(async () => {
const circleBtn = page.getByRole('button', { name: 'circle Circle' })
return circleBtn.getAttribute('aria-pressed')
})
.toBe('true')
await circleCenterClk()
await editor.expectEditor.toContain(
'profile003 = circle(sketch003, center'
)
await circleRadiusClk()
await editor.expectEditor.toContain(
'profile003 = circle(sketch003, center = [-25.77, 10.97], radius = 1.85)'
)
})
})
test('Can sketch on face when user defined function was used in the sketch', async ({
page,
homePage,
}) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
// Checking for a regression that performs a sketch when a user defined function
// is declared at the top of the file and used in the sketch that is being drawn on.
// fn in2mm is declared at the top of the file and used rail which does a an extrusion with the function.
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`fn in2mm(@inches) {
return inches * 25.4
}
railTop = in2mm(.748)
railSide = in2mm(.024)
railBaseWidth = in2mm(.612)
railWideWidth = in2mm(.835)
railBaseLength = in2mm(.200)
railClampable = in2mm(.200)
rail = startSketchOn(XZ)
|> startProfile(at = [-railTop / 2, railClampable + railBaseLength])
|> line(endAbsolute = [
railTop / 2,
railClampable + railBaseLength
])
|> line(endAbsolute = [
railWideWidth / 2,
railClampable / 2 + railBaseLength
], tag = $seg01)
|> line(endAbsolute = [railTop / 2, railBaseLength])
|> line(endAbsolute = [railBaseWidth / 2, railBaseLength])
|> line(endAbsolute = [railBaseWidth / 2, 0])
|> line(endAbsolute = [-railBaseWidth / 2, 0])
|> line(endAbsolute = [-railBaseWidth / 2, railBaseLength])
|> line(endAbsolute = [-railTop / 2, railBaseLength])
|> line(endAbsolute = [
-railWideWidth / 2,
railClampable / 2 + railBaseLength
])
|> line(endAbsolute = [
-railTop / 2,
railClampable + railBaseLength
])
|> close()
|> extrude(length = in2mm(2))`
)
})
const center = { x: 600, y: 250 }
const rectangleSize = 20
await homePage.goToModelingScene()
// Start a sketch
await page.getByRole('button', { name: 'Start Sketch' }).click()
// Click the top face of this rail
await page.mouse.click(center.x, center.y)
await page.waitForTimeout(1000)
// Draw a rectangle
// top left
await page.mouse.click(center.x - rectangleSize, center.y - rectangleSize)
await page.waitForTimeout(250)
// top right
await page.mouse.click(center.x + rectangleSize, center.y - rectangleSize)
await page.waitForTimeout(250)
// bottom right
await page.mouse.click(center.x + rectangleSize, center.y + rectangleSize)
await page.waitForTimeout(250)
// bottom left
await page.mouse.click(center.x - rectangleSize, center.y + rectangleSize)
await page.waitForTimeout(250)
// top left
await page.mouse.click(center.x - rectangleSize, center.y - rectangleSize)
await page.waitForTimeout(250)
// exit sketch
await page.getByRole('button', { name: 'Exit Sketch' }).click()
// Check execution is done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
})
test('Can delete a single segment line with keyboard', async ({
page,
scene,
homePage,
cmdBar,
editor,
}) => {
const u = await getUtils(page)
const viewportSize = { width: 1100, height: 750 }
await page.setBodyDimensions(viewportSize)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = mm)
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [0, 0])
|> xLine(length = 25.0)
|> yLine(length = 5.0)
|> line(end = [-22.0, 12.0])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`
)
})
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// Enter sketch mode
await page.mouse.dblclick(654, 450)
await page.waitForTimeout(1000)
// Select the third line
await page.mouse.click(918, 90)
await page.waitForTimeout(1000)
// Delete with backspace
await page.keyboard.press('Delete')
// Wait for engine re-execution to complete
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
// Validate the editor code no longer contains the deleted line
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [0, 0])
|> xLine(length = 25.0)
|> yLine(length = 5.0)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
`,
{ shouldNormalise: true }
)
})
})
test.describe(`Sketching with offset planes`, () => {
test(`Can select an offset plane to sketch on`, async ({
context,
page,
scene,
toolbar,
editor,
homePage,
}) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
offsetPlane001 = offsetPlane(XY, offset = 10)`
)
})
await homePage.goToModelingScene()
const [planeClick, planeHover] = scene.makeMouseHelpers(650, 200)
await test.step(`Start sketching on the offset plane`, async () => {
await toolbar.startSketchPlaneSelection()
await test.step(`Hovering should highlight code`, async () => {
await planeHover()
await editor.expectState({
activeLines: [`@settings(defaultLengthUnit = in)`],
diagnostics: [],
highlightedCode: 'offsetPlane(XY, offset = 10)',
})
})
await test.step(`Clicking should select the plane and enter sketch mode`, async () => {
await planeClick()
// Have to wait for engine-side animation to finish
await page.waitForTimeout(600)
await expect(toolbar.lineBtn).toBeEnabled()
await editor.expectEditor.toContain('startSketchOn(offsetPlane001)')
await editor.expectState({
activeLines: [`@settings(defaultLengthUnit = in)`],
diagnostics: [],
highlightedCode: '',
})
})
})
})
})
test.describe('multi-profile sketching', () => {
test(`test it removes half-finished expressions when changing tools in sketch mode`, async ({
context,
page,
scene,
toolbar,
editor,
homePage,
cmdBar,
}) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
'persistCode',
`yo = 5
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [121.52, 168.25])
|> line(end = [115.04, 113.61])
|> line(end = [130.87, -97.79])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile002 = startProfile(sketch001, at = [117.2, 56.08])
|> line(end = [166.82, 25.89])
|> yLine(length = -107.86)
`
)
})
const [continueProfile2Clk] = scene.makeMouseHelpers(954, 282)
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await (await toolbar.getFeatureTreeOperation('Sketch', 0)).dblclick()
await page.waitForTimeout(600)
const [circlePoint1] = scene.makeMouseHelpers(700, 200)
await test.step('equip circle tool and click first point', async () => {
// await page.waitForTimeout(100)
await expect
.poll(async () => {
await toolbar.circleBtn.click()
return toolbar.circleBtn.getAttribute('aria-pressed')
})
.toBe('true')
await page.waitForTimeout(100)
await circlePoint1()
await editor.expectEditor.toContain(
'profile003 = circle(sketch001, center = ['
)
})
await test.step('equip line tool and verify circle code is removed', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('profile003 = circle(')
})
const [circle3Point1] = scene.makeMouseHelpers(650, 200)
const [circle3Point2] = scene.makeMouseHelpers(750, 200)
// const [circle3Point3] = scene.makeMouseHelpers(700, 150)
await test.step('equip three point circle tool and click first two points', async () => {
await toolbar.selectCircleThreePoint()
await page.waitForTimeout(100)
await circle3Point1()
await page.waitForTimeout(100)
await circle3Point2()
await editor.expectEditor.toContain('profile003 = circleThreePoint(')
})
await test.step('equip line tool and verify three-point circle code is removed', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('profile003 = circleThreePoint(')
})
await test.step('equip three-point-arc tool and click first two points', async () => {
await page.waitForTimeout(200)
await toolbar.selectThreePointArc()
await page.waitForTimeout(200)
await circle3Point1()
await page.waitForTimeout(200)
await circle3Point2()
await editor.expectEditor.toContain('arc(')
})
await test.step('equip line tool and verify three-point-arc code is removed after second click', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('arc(')
})
const [cornerRectPoint1] = scene.makeMouseHelpers(600, 300)
await test.step('equip corner rectangle tool and click first point', async () => {
await toolbar.rectangleBtn.click()
await page.waitForTimeout(100)
await cornerRectPoint1()
await editor.expectEditor.toContain('profile004 = startProfile(')
})
await test.step('equip line tool and verify corner rectangle code is removed', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('profile004 = startProfile(')
})
const [centerRectPoint1] = scene.makeMouseHelpers(700, 300)
await test.step('equip center rectangle tool and click first point', async () => {
await toolbar.selectCenterRectangle()
await page.waitForTimeout(100)
await centerRectPoint1()
await editor.expectEditor.toContain('profile004 = startProfile(')
})
await test.step('equip line tool and verify center rectangle code is removed', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('profile004 = startProfile(')
})
await test.step('continue profile002 with the three point arc tool, and then switch back to the line tool to verify it only removes the last expression in the pipe', async () => {
await toolbar.selectThreePointArc()
await page.waitForTimeout(200)
await continueProfile2Clk()
await page.waitForTimeout(200)
await circle3Point1()
await editor.expectEditor.toContain('arc(')
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain('arc(')
await editor.expectEditor.toContain('profile002')
})
})
test(`snapToProfile start only works for current profile`, async ({
context,
page,
scene,
toolbar,
editor,
homePage,
cmdBar,
}) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ)
profile002 = startProfile(sketch001, at = [40.68, 87.67])
|> xLine(length = 239.17)
profile003 = startProfile(sketch001, at = [206.63, -56.73])
|> xLine(length = -156.32)
`
)
})
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const [onSegmentClick] = scene.makeMouseHelpers(604, 349)
const [endOfLowerSegClick, endOfLowerSegMove] = scene.makeMouseHelpers(
697,
360
)
const [profileStartOfHigherSegClick, profileStartOfHigherSegMove] =
scene.makeMouseHelpers(677, 78)
const tanArcLocation = { x: 624, y: 340 } as const
await test.step('enter sketch mode', async () => {
await onSegmentClick({ shouldDbClick: true })
await page.waitForTimeout(600)
})
const codeFromTangentialArc = ` |> tangentialArc(endAbsolute = [39.49, 88.22])`
await test.step('check that tangential tool does not snap to other profile starts', async () => {
await toolbar.selectTangentialArc()
await page.waitForTimeout(1000)
await endOfLowerSegMove()
await page.waitForTimeout(1000)
await endOfLowerSegClick()
await page.waitForTimeout(1000)
await profileStartOfHigherSegClick()
await page.waitForTimeout(1000)
await editor.expectEditor.toContain(codeFromTangentialArc)
await editor.expectEditor.not.toContain(
`[profileStartX(%), profileStartY(%)]`
)
})
await test.step('remove tangential arc code to reset', async () => {
await scene.expectPixelColor(TEST_COLORS.WHITE, tanArcLocation, 15)
await editor.replaceCode(codeFromTangentialArc, '')
// check pixel is now gray at tanArcLocation to verify code has executed
await scene.expectPixelColor([26, 26, 26], tanArcLocation, 15)
await editor.expectEditor.not.toContain(
`tangentialArc(endAbsolute = [39.49, 88.22])`
)
})
await test.step('check that tangential tool does snap to current profile start', async () => {
await expect
.poll(async () => {
await toolbar.lineBtn.click()
return toolbar.lineBtn.getAttribute('aria-pressed')
})
.toBe('true')
await profileStartOfHigherSegMove()
await endOfLowerSegMove()
await endOfLowerSegClick()
await profileStartOfHigherSegClick()
await editor.expectEditor.toContain('line(end = [-10.82, 144.95])')
await editor.expectEditor.not.toContain(
`[profileStartX(%), profileStartY(%)]`
)
})
})
test('can enter sketch mode for sketch with no profiles', async ({
scene,
toolbar,
editor,
cmdBar,
page,
homePage,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn(XY)
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
// open feature tree and double click the first sketch
await (await toolbar.getFeatureTreeOperation('Sketch', 0)).dblclick()
await page.waitForTimeout(600)
// click in the scene twice to add a segment
const [startProfile1] = scene.makeMouseHelpers(658, 140)
const [segment1Clk] = scene.makeMouseHelpers(701, 200)
// wait for line to be aria pressed
await expect
.poll(async () => toolbar.lineBtn.getAttribute('aria-pressed'))
.toBe('true')
await startProfile1()
await editor.expectEditor.toContain(`profile001 = startProfile`)
await segment1Clk()
await editor.expectEditor.toContain(`|> line(end`)
})
test('can delete all profiles in sketch mode and user can still equip a tool and draw something', async ({
scene,
toolbar,
editor,
page,
homePage,
}) => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
await toolbar.startSketchPlaneSelection()
await selectXZPlane()
// timeout wait for engine animation is unavoidable
await page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn(XZ)`)
const [startProfile1] = scene.makeMouseHelpers(568, 70)
const [segment1Clk] = scene.makeMouseHelpers(701, 78)
const [segment2Clk] = scene.makeMouseHelpers(745, 189)
await test.step('add two segments', async () => {
await startProfile1()
await editor.expectEditor.toContain(
`profile001 = startProfile(sketch001, at = [4.61, 12.21])`
)
await segment1Clk()
await editor.expectEditor.toContain(`|> line(end`)
await segment2Clk()
await editor.expectEditor.toContain(`|> line(end = [2.98, -7.52])`)
})
await test.step('delete all profiles', async () => {
await editor.replaceCode('', 'sketch001 = startSketchOn(XZ)\n')
await page.waitForTimeout(600) // wait for deferred execution
})
await test.step('equip circle and draw it', async () => {
await toolbar.circleBtn.click()
await page.mouse.click(700, 200)
await page.mouse.click(750, 200)
await editor.expectEditor.toContain('circle(sketch001, center = [')
})
})
test('Can add multiple profiles to a sketch (all tool types)', async ({
scene,
toolbar,
editor,
page,
homePage,
}) => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
const [startProfile1] = scene.makeMouseHelpers(568, 70)
const [endLineStartTanArc] = scene.makeMouseHelpers(701, 78)
const [endArcStartLine] = scene.makeMouseHelpers(745, 189)
const [startProfile2] = scene.makeMouseHelpers(782, 80)
const [profile2Point2] = scene.makeMouseHelpers(921, 90)
const [profile2Point3] = scene.makeMouseHelpers(953, 178)
const [circle1Center] = scene.makeMouseHelpers(842, 147)
const [circle1Radius] = scene.makeMouseHelpers(870, 171)
const [circle2Center] = scene.makeMouseHelpers(850, 222)
const [circle2Radius] = scene.makeMouseHelpers(843, 230)
const [crnRect1point1] = scene.makeMouseHelpers(583, 205)
const [crnRect1point2] = scene.makeMouseHelpers(618, 320)
const [crnRect2point1] = scene.makeMouseHelpers(663, 215)
const [crnRect2point2] = scene.makeMouseHelpers(744, 276)
const [cntrRect1point1] = scene.makeMouseHelpers(624, 387)
const [cntrRect1point2] = scene.makeMouseHelpers(676, 355)
const [cntrRect2point1] = scene.makeMouseHelpers(785, 332)
const [cntrRect2point2] = scene.makeMouseHelpers(808, 286)
const [circle3Point1p1, circle3Point1p1Move] = scene.makeMouseHelpers(
630,
465
)
const [circle3Point1p2, circle3Point1p2Move] = scene.makeMouseHelpers(
673,
340
)
const [circle3Point1p3, circle3Point1p3Move] = scene.makeMouseHelpers(
734,
414
)
const [circle3Point2p1, circle3Point2p1Move] = scene.makeMouseHelpers(
876,
351
)
const [circle3Point2p2, circle3Point2p2Move] = scene.makeMouseHelpers(
875,
279
)
const [circle3Point2p3, circle3Point2p3Move] = scene.makeMouseHelpers(
834,
306
)
await toolbar.startSketchPlaneSelection()
await selectXZPlane()
// timeout wait for engine animation is unavoidable
await page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn(XZ)`)
await test.step('Create a close profile stopping mid profile to equip the tangential arc, then three-point arc, and then back to the line tool', async () => {
await startProfile1()
await editor.expectEditor.toContain(
`profile001 = startProfile(sketch001, at = [4.61, 12.21])`
)
await endLineStartTanArc()
await editor.expectEditor.toContain(`|> line(end = [9.02, -0.55])`)
await toolbar.selectTangentialArc()
await page.waitForTimeout(300)
await page.mouse.click(745, 359)
await page.waitForTimeout(300)
await endLineStartTanArc({ delay: 544 })
await endArcStartLine()
await editor.expectEditor.toContain(
`|> tangentialArc(endAbsolute = [16.61, 4.14])`
)
// Add a three-point arc segment
await toolbar.selectThreePointArc()
await page.waitForTimeout(300)
// select end of profile again
await endLineStartTanArc()
await page.waitForTimeout(300)
// Define points for the three-point arc
const [threePointInterior, threePointInteriorMove] =
scene.makeMouseHelpers(600, 200)
const [threePointEnd, threePointEndMove] = scene.makeMouseHelpers(
590,
270
)
// Create the three-point arc
await page.waitForTimeout(300)
await threePointInteriorMove()
await threePointInterior()
await page.waitForTimeout(300)
await threePointEndMove()
await threePointEnd()
await page.waitForTimeout(300)
// Verify the three-point arc was created correctly
await editor.expectEditor.toContain(`arc(`)
await editor.expectEditor.toContain(`interiorAbsolute`)
await editor.expectEditor.toContain(`endAbsolute`)
// Switch back to line tool to continue
await toolbar.lineBtn.click()
await page.waitForTimeout(300)
// Continue with the original line segment
await threePointEnd()
await page.waitForTimeout(300)
await page.mouse.click(572, 110)
await editor.expectEditor.toContain(`|> line(end = [-1.22, 10.85])`)
await startProfile1()
await editor.expectEditor.toContain(
`|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`,
{ shouldNormalise: true }
)
await page.waitForTimeout(300)
})
await test.step('Without unequipping from the last step, make another profile, and one that is not closed', async () => {
await startProfile2()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
`profile002 = startProfile(sketch001, at = [19.12, 11.53])`
)
await profile2Point2()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(`|> line(end = [9.43, -0.68])`)
await profile2Point3()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(`|> line(end = [2.17, -5.97])`)
})
await test.step('create two circles in a row without unequip', async () => {
await toolbar.circleBtn.click()
await circle1Center()
await page.waitForTimeout(300)
await circle1Radius({ delay: 500 })
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
`profile003 = circle(sketch001, center = [23.19, 6.98], radius = 2.5)`
)
await test.step('hover in empty space to wait for overlays to get out of the way', async () => {
await page.mouse.move(951, 223)
await page.waitForTimeout(1000)
})
await circle2Center()
await page.waitForTimeout(300)
await circle2Radius()
await editor.expectEditor.toContain(
`profile004 = circle(sketch001, center = [23.74, 1.9], radius = 0.72)`
)
})
await test.step('create two corner rectangles in a row without unequip', async () => {
await expect
.poll(async () => {
await toolbar.rectangleBtn.click()
return toolbar.rectangleBtn.getAttribute('aria-pressed')
})
.toBe('true')
await crnRect1point1()
await editor.expectEditor.toContain(
`profile005 = startProfile(sketch001, at = [5.63, 3.05])`
)
await crnRect1point2()
await editor.expectEditor.toContain(
`|> angledLine(angle = 0, length = 2.37, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 7.8)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`.replaceAll('\n', '')
)
await crnRect2point1()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
`profile006 = startProfile(sketch001, at = [11.05, 2.37])`
)
await crnRect2point2()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
`|> angledLine(angle = 0, length = 5.49, tag = $rectangleSegmentA002)
|> angledLine(angle = segAng(rectangleSegmentA002) - 90, length = 4.14)
|> angledLine(angle = segAng(rectangleSegmentA002), length = -segLen(rectangleSegmentA002))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`.replaceAll('\n', '')
)
})
await test.step('create two center rectangles in a row without unequip', async () => {
await toolbar.selectCenterRectangle()
await cntrRect1point1()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
`profile007 = startProfile(sketch001, at = [8.41, -9.29])`
)
await cntrRect1point2()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
`|> angledLine(angle = 0, length = 7.06, tag = $rectangleSegmentA003)
|> angledLine(angle = segAng(rectangleSegmentA003) + 90, length = 4.34)
|> angledLine(angle = segAng(rectangleSegmentA003), length = -segLen(rectangleSegmentA003))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`.replaceAll('\n', '')
)
await page.waitForTimeout(300)
await cntrRect2point1()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
`profile008 = startProfile(sketch001, at = [19.33, -5.56])`
)
await cntrRect2point2()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
`|> angledLine(angle = 0, length = 3.12, tag = $rectangleSegmentA004)
|> angledLine(angle = segAng(rectangleSegmentA004) + 90, length = 6.24)
|> angledLine(angle = segAng(rectangleSegmentA004), length = -segLen(rectangleSegmentA004))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`.replaceAll('\n', '')
)
})
await test.step('create two circle-three-points in a row without an unequip', async () => {
await toolbar.selectCircleThreePoint()
await circle3Point1p1Move()
await circle3Point1p1()
await page.waitForTimeout(300)
await circle3Point1p2Move()
await circle3Point1p2()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
`profile009 = circleThreePoint(
sketch001,
p1 = [8.82, -14.58],
p2 = [11.73, -6.1],
p3 = [11.83, -6],
)`,
{ shouldNormalise: true }
)
await circle3Point1p3Move()
await circle3Point1p3()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
`profile009 = circleThreePoint(
sketch001,
p1 = [8.82, -14.58],
p2 = [11.73, -6.1],
p3 = [15.87, -11.12],
)`,
{ shouldNormalise: true }
)
await circle3Point2p1Move()
await circle3Point2p1()
await page.waitForTimeout(300)
await circle3Point2p2Move()
await circle3Point2p2()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
`profile010 = circleThreePoint(
sketch001,
p1 = [25.5, -6.85],
p2 = [25.43, -1.97],
p3 = [25.53, -1.87],
)`,
{ shouldNormalise: true }
)
await circle3Point2p3Move()
await circle3Point2p3()
await page.waitForTimeout(300)
await editor.expectEditor.toContain(
`profile010 = circleThreePoint(
sketch001,
p1 = [25.5, -6.85],
p2 = [25.43, -1.97],
p3 = [22.65, -3.8],
)`,
{ shouldNormalise: true }
)
})
await test.step('create three-point arcs in a row without an unequip', async () => {
// Define points for the first three-point arc
const [arc1Point1, arc1Point1Move] = scene.makeMouseHelpers(700, 397)
const [arc1Point2, arc1Point2Move] = scene.makeMouseHelpers(724, 346)
const [arc1Point3, arc1Point3Move] = scene.makeMouseHelpers(785, 415)
// Define points for the second three-point arc
const [arc2Point1, arc2Point1Move] = scene.makeMouseHelpers(792, 225)
const [arc2Point2, arc2Point2Move] = scene.makeMouseHelpers(820, 207)
const [arc2Point3, arc2Point3Move] = scene.makeMouseHelpers(905, 229)
// Select the three-point arc tool
await toolbar.selectThreePointArc()
// Create the first three-point arc
await arc1Point1Move()
await arc1Point1()
await page.waitForTimeout(300)
await arc1Point2Move()
await arc1Point2()
await page.waitForTimeout(300)
await arc1Point3Move()
await arc1Point3()
await page.waitForTimeout(300)
// Verify the first three-point arc was created correctly
await editor.expectEditor.toContain(
`profile011 = startProfile(sketch001, at = [13.56, -9.97])
|> arc(interiorAbsolute = [15.19, -6.51], endAbsolute = [19.33, -11.19])`,
{ shouldNormalise: true }
)
// Create the second three-point arc
await arc2Point1Move()
await arc2Point1()
await page.waitForTimeout(300)
await arc2Point2Move()
await arc2Point2()
await page.waitForTimeout(300)
await arc2Point3Move()
await arc2Point3()
await page.waitForTimeout(300)
// Verify the second three-point arc was created correctly
await editor.expectEditor.toContain(
` |> arc(interiorAbsolute = [19.8, 1.7], endAbsolute = [21.7, 2.92])
|> arc(interiorAbsolute = [27.47, 1.42], endAbsolute = [27.57, 1.52])`,
{ shouldNormalise: true }
)
})
await test.step('double check that three-point arc can be unequipped', async () => {
// this was tested implicitly for other tools, but not for three-point arc since it's last
await page.waitForTimeout(300)
await expect
.poll(async () => {
await toolbar.lineBtn.click()
return toolbar.lineBtn.getAttribute('aria-pressed')
})
.toBe('true')
})
})
test('Can edit a sketch with multiple profiles, dragging segments to edit them, and adding one new profile', async ({
homePage,
scene,
toolbar,
editor,
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [6.24, 4.54])
|> line(end = [-0.41, 6.99])
|> line(end = [8.61, 0.74])
|> line(end = [10.99, -5.22])
profile002 = startProfile(sketch001, at = [11.19, 5.02])
|> angledLine(angle = 0, length = 10.78, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 4.14)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = circle(sketch001, center = [6.92, -4.2], radius = 3.16)
profile004 = circleThreePoint(sketch001, p1 = [13.44, -6.8], p2 = [13.39, -2.07], p3 = [18.75, -4.41])
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
// The text to prompt popover gets in the way of pointOnSegment click otherwise
const moveToClearToolBarPopover = scene.makeMouseHelpers(590, 500)[1]
const [pointOnSegment] = scene.makeMouseHelpers(590, 141)
const [profileEnd] = scene.makeMouseHelpers(970, 105)
const profileEndMv = scene.makeMouseHelpers(951, 101)[1]
const [newProfileEnd] = scene.makeMouseHelpers(764, 104)
const dragSegmentTo = scene.makeMouseHelpers(850, 104)[1]
const rectHandle = scene.makeMouseHelpers(901, 150)[1]
const rectDragTo = scene.makeMouseHelpers(901, 180)[1]
const circleEdge = scene.makeMouseHelpers(691, 331)[1]
const dragCircleTo = scene.makeMouseHelpers(720, 331)[1]
const [rectStart] = scene.makeMouseHelpers(794, 322)
const [rectEnd] = scene.makeMouseHelpers(757, 395)
const [circ3PStart] = scene.makeMouseHelpers(854, 332)
const [circ3PEnd] = scene.makeMouseHelpers(870, 275)
await test.step('enter sketch and setup', async () => {
await moveToClearToolBarPopover()
await page.waitForTimeout(1000)
await pointOnSegment({ shouldDbClick: true })
await page.waitForTimeout(2000)
await toolbar.lineBtn.click()
await page.waitForTimeout(100)
})
await test.step('extend existing profile', async () => {
await profileEnd()
await page.waitForTimeout(100)
await newProfileEnd()
await editor.expectEditor.toContain(`|> line(end = [-11.35, 0.73])`)
await toolbar.lineBtn.click()
await page.waitForTimeout(100)
})
await test.step('edit existing profile', async () => {
await profileEndMv()
await page.mouse.down()
await dragSegmentTo()
await page.mouse.up()
await editor.expectEditor.toContain(`line(end = [4.22, -4.49])`)
})
await test.step('edit existing rect', async () => {
await rectHandle()
await page.mouse.down()
await rectDragTo()
await page.mouse.up()
await editor.expectEditor.toContain(
`angledLine(angle = -7, length = 10.27, tag = $rectangleSegmentA001)`
)
})
await test.step('edit existing circl', async () => {
await circleEdge()
await page.mouse.down()
await dragCircleTo()
await page.mouse.up()
await editor.expectEditor.toContain(
`profile003 = circle(sketch001, center = [6.92, -4.2], radius = 4.81)`
)
})
await test.step('edit existing circle three point', async () => {
await circ3PStart()
await page.mouse.down()
await circ3PEnd()
await page.mouse.up()
await editor.expectEditor.toContain(
`profile004 = circleThreePoint(
sketch001,
p1 = [13.44, -6.8],
p2 = [13.39, -2.07],
p3 = [19.73, -1.33],
)`,
{ shouldNormalise: true }
)
})
await test.step('add new profile', async () => {
await toolbar.rectangleBtn.click()
await page.waitForTimeout(100)
await rectStart()
await editor.expectEditor.toContain(
`profile005 = startProfile(sketch001, at = [15.68, -3.84])`
)
await page.waitForTimeout(100)
await rectEnd()
await editor.expectEditor.toContain(
`|> angledLine(angle = 180, length = 1.97, tag = $rectangleSegmentA002)
|> angledLine(angle = segAng(rectangleSegmentA002) + 90, length = 3.89)
|> angledLine(angle = segAng(rectangleSegmentA002), length = -segLen(rectangleSegmentA002))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`.replaceAll('\n', '')
)
})
})
test('Can delete a profile in the editor while is sketch mode, and sketch mode does not break, can ctrl+z to undo after constraint with variable was added', async ({
scene,
toolbar,
editor,
cmdBar,
page,
homePage,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [6.24, 4.54])
|> line(end = [-0.41, 6.99])
|> line(end = [8.61, 0.74])
|> line(end = [10.99, -5.22])
profile002 = startProfile(sketch001, at = [11.19, 5.02])
|> angledLine(angle = 0, length = 10.78, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 4.14)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = circle(sketch001, center = [6.92, -4.2], radius = 3.16)
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
// The text to prompt popover gets in the way of pointOnSegment click otherwise
const moveToClearToolBarPopover = scene.makeMouseHelpers(590, 500)[1]
const [pointOnSegment] = scene.makeMouseHelpers(590, 141)
const [segment1Click] = scene.makeMouseHelpers(616, 131)
const sketchIsDrawnProperly = async () => {
await test.step('check the sketch is still drawn properly', async () => {
await page.waitForTimeout(200)
await scene.expectPixelColor([255, 255, 255], { x: 617, y: 163 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 629, y: 331 }, 15)
})
}
await test.step('enter sketch and setup', async () => {
await moveToClearToolBarPopover()
await pointOnSegment({ shouldDbClick: true })
await page.waitForTimeout(600)
})
await test.step('select and delete code for a profile', async () => {})
await page.getByText('close()').click()
await page.keyboard.down('Shift')
for (let i = 0; i < 11; i++) {
await page.keyboard.press('ArrowUp')
}
await page.keyboard.press('Home')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
await sketchIsDrawnProperly()
await test.step('add random new var between profiles', async () => {
await page.keyboard.type('myVar = 5')
await page.keyboard.press('Enter')
// If this timeout isn't long enough, the test breaks.
// TODO: fix https://github.com/KittyCAD/modeling-app/issues/5437
await page.waitForTimeout(3_000)
})
await sketchIsDrawnProperly()
await test.step('Adding a constraint with a variable, and than ctrl-z-ing which will remove the variable again does not break sketch mode', async () => {
await expect(async () => {
await segment1Click()
await editor.expectState({
diagnostics: [],
activeLines: ['|>line(end = [-0.41,6.99])'],
highlightedCode: 'line(end = [-0.41,6.99])',
})
}).toPass({ timeout: 30_000, intervals: [1500] })
await toolbar.lengthConstraintBtn.click()
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain('length001 = 7')
// wait for execute defer
await page.waitForTimeout(600)
await sketchIsDrawnProperly()
await page.keyboard.down('Meta')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Meta')
await editor.expectEditor.not.toContain('length001 = 7')
await sketchIsDrawnProperly()
})
})
test('can enter sketch when there is an extrude', async ({
homePage,
scene,
toolbar,
page,
cmdBar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [-63.43, 193.08])
|> line(end = [168.52, 149.87])
|> line(end = [190.29, -39.18])
|> tangentialArc(endAbsolute = [319.63, 129.65])
|> line(end = [-217.65, -21.76])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfile(sketch001, at = [16.79, 38.24])
|> angledLine(angle = 0, length = 182.82, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 105.71)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile004 = circle(
sketch001,
center = [280.45, 47.57],
radius = 55.26
)
extrude002 = extrude(profile001, length = 50)
extrude001 = extrude(profile003, length = 5)
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const [pointOnSegment] = scene.makeMouseHelpers(574, 207)
await pointOnSegment()
await toolbar.editSketch()
// wait for engine animation
await page.waitForTimeout(600)
await test.step('check the sketch is still drawn properly', async () => {
await Promise.all([
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 596, y: 165 }, 15),
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 641, y: 220 }, 15),
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 763, y: 214 }, 15),
])
})
})
test('exit new sketch without drawing anything should not be a problem', async ({
homePage,
scene,
toolbar,
editor,
cmdBar,
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
myVar = 5`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await page.waitForTimeout(5000)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
await toolbar.startSketchPlaneSelection()
await selectXZPlane()
// timeout wait for engine animation is unavoidable
await page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn(XZ)`)
await toolbar.exitSketch()
await editor.expectEditor.not.toContain(`sketch001 = startSketchOn(XZ)`)
await test.step("still renders code, hasn't got into a weird state", async () => {
await editor.replaceCode(
'myVar = 5',
`myVar = 5
sketch001 = startSketchOn(XZ)
profile001 = circle(
sketch001,
center = [12.41, 3.87],
radius = myVar
)`
)
await scene.settled(cmdBar)
await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15)
})
})
test('A sketch with only "startProfileAt" and no segments should still be able to be continued', async ({
homePage,
scene,
toolbar,
editor,
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [85.19, 338.59])
|> line(end = [213.3, -94.52])
|> line(end = [-230.09, -55.34])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch002 = startSketchOn(XY)
profile002 = startProfile(sketch002, at = [85.81, 52.55])
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const [startProfileAt] = scene.makeMouseHelpers(606, 184)
const [nextPoint] = scene.makeMouseHelpers(763, 130)
await page.getByText('startProfile(sketch002, at = [85.81, 52.55])').click()
await toolbar.editSketch(1)
// timeout wait for engine animation is unavoidable
await page.waitForTimeout(600)
// equip line tool
await toolbar.lineBtn.click()
await page.waitForTimeout(100)
await startProfileAt()
await page.waitForTimeout(100)
await nextPoint()
await editor.expectEditor.toContain(`|> line(end = [126.05, 44.12])`)
})
test('old style sketch all in one pipe (with extrude) will break up to allow users to add a new profile to the same sketch', async ({
homePage,
scene,
toolbar,
editor,
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
thePart = startSketchOn(XZ)
|> startProfile(at = [7.53, 10.51])
|> line(end = [12.54, 1.83])
|> line(end = [6.65, -6.91])
|> line(end = [-6.31, -8.69])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(thePart, length = 75)
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const [objClick] = scene.makeMouseHelpers(565, 343)
const [profilePoint1] = scene.makeMouseHelpers(609, 289)
const [profilePoint2] = scene.makeMouseHelpers(714, 389)
await test.step('enter sketch and setup', async () => {
await objClick()
await toolbar.editSketch()
// timeout wait for engine animation is unavoidable
await page.waitForTimeout(600)
})
await test.step('expect code to match initial conditions still', async () => {
await editor.expectEditor.toContain(
`thePart = startSketchOn(XZ) |> startProfile(at = [7.53, 10.51])`
)
})
await test.step('equiping the line tool should break up the pipe expression', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)thePart = startProfile(sketch001, at = [7.53, 10.51])`
)
})
await test.step('can continue on to add a new profile to this sketch', async () => {
await profilePoint1()
await editor.expectEditor.toContain(
`profile001 = startProfile(sketch001, at = [19.69, -7.05])`
)
await profilePoint2()
await editor.expectEditor.toContain(`|> line(end = [18.97, -18.06])`)
})
})
test('Can enter sketch on sketch of wall and cap for segment, solid2d, extrude-wall, extrude-cap selections', async ({
homePage,
scene,
toolbar,
editor,
page,
cmdBar,
}) => {
// TODO this test should include a test for selecting revolve walls and caps
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [6.71, -3.66])
|> line(end = [2.65, 9.02], tag = $seg02)
|> line(end = [3.73, -9.36], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = 20)
sketch002 = startSketchOn(extrude001, face = seg01)
profile002 = startProfile(sketch002, at = [0.75, 13.46])
|> line(end = [4.52, 3.79])
|> line(end = [5.98, -2.81])
profile003 = startProfile(sketch002, at = [3.19, 13.3])
|> angledLine(angle = 0, length = 6.64, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 2.81)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile004 = startProfile(sketch002, at = [3.15, 9.39])
|> xLine(length = 6.92)
|> line(end = [-7.41, -2.85])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile005 = circle(sketch002, center = [5.15, 4.34], radius = 1.66)
profile006 = startProfile(sketch002, at = [9.65, 3.82])
|> line(end = [2.38, 5.62])
|> line(end = [2.13, -5.57])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
revolve001 = revolve(
profile004,
angle = 45,
axis = getNextAdjacentEdge(seg01)
)
extrude002 = extrude(profile006, length = 4)
sketch003 = startSketchOn(-XZ)
profile007 = startProfile(sketch003, at = [4.8, 7.55])
|> line(end = [7.39, 2.58])
|> line(end = [7.02, -2.85])
profile008 = startProfile(sketch003, at = [5.54, 5.49])
|> line(end = [6.34, 2.64])
|> line(end = [6.33, -2.96])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile009 = startProfile(sketch003, at = [5.23, 1.95])
|> line(end = [6.8, 2.17])
|> line(end = [7.34, -2.75])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile010 = circle(
sketch003,
center = [7.18, -2.11],
radius = 2.67
)
profile011 = startProfile(sketch003, at = [5.07, -6.39])
|> angledLine(angle = 0, length = 4.54, tag = $rectangleSegmentA002)
|> angledLine(angle = segAng(rectangleSegmentA002) - 90, length = 4.17)
|> angledLine(angle = segAng(rectangleSegmentA002), length = -segLen(rectangleSegmentA002))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude003 = extrude(profile011, length = 2.5)
// TODO this breaks the test,
// revolve002 = revolve(profile008, angle = 45, axis = seg02)
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const camPositionForSelectingSketchOnWallProfiles = () =>
scene.moveCameraTo(
{ x: 834, y: -680, z: 534 },
{ x: -54, y: -476, z: 148 }
)
const wallSelectionOptions = [
{
title: 'select wall segment',
selectClick: scene.makeMouseHelpers(598, 211)[0],
},
{
title: 'select wall solid 2d',
selectClick: scene.makeMouseHelpers(677, 236)[0],
},
{
title: 'select wall circle',
selectClick: scene.makeMouseHelpers(811, 247)[0],
},
{
title: 'select wall extrude wall',
selectClick: scene.makeMouseHelpers(793, 136)[0],
},
{
title: 'select wall extrude cap',
selectClick: scene.makeMouseHelpers(836, 103)[0],
},
] as const
const verifyWallProfilesAreDrawn = async () =>
test.step('verify wall profiles are drawn', async () => {
await Promise.all([
// open polygon
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 599, y: 168 }, 15),
// closed polygon
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 656, y: 171 }, 15),
// revolved profile
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 655, y: 264 }, 15),
// extruded profile
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 808, y: 396 }, 15),
// circle (When entering via the circle, it's selected and therefore blue)
scene.expectPixelColor(
[TEST_COLORS.WHITE, TEST_COLORS.BLUE],
{ x: 742, y: 386 },
15
),
])
})
await test.step('select wall profiles', async () => {
for (const { title, selectClick } of wallSelectionOptions) {
await test.step(title, async () => {
await camPositionForSelectingSketchOnWallProfiles()
await selectClick()
await toolbar.editSketch(1)
await page.waitForTimeout(600)
await verifyWallProfilesAreDrawn()
await toolbar.exitSketchBtn.click()
await page.waitForTimeout(100)
})
}
})
/* FIXME: the cap part of this test is insanely flaky, and I'm not sure
* why.
* await test.step('select cap profiles', async () => {
for (const { title, selectClick } of capSelectionOptions) {
await test.step(title, async () => {
await camPositionForSelectingSketchOnCapProfiles()
await page.waitForTimeout(100)
await selectClick()
await toolbar.editSketch()
await page.waitForTimeout(600)
await verifyCapProfilesAreDrawn()
await toolbar.exitSketchBtn.click()
await page.waitForTimeout(100)
})
}
}) */
})
test('Can enter sketch loft edges, base and continue sketch', async ({
homePage,
scene,
toolbar,
editor,
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [34, 42.66])
|> line(end = [102.65, 151.99])
|> line(end = [76, -138.66])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
plane001 = offsetPlane(XZ, offset = 50)
sketch002 = startSketchOn(plane001)
profile002 = startProfile(sketch002, at = [39.43, 172.21])
|> xLine(length = 183.99)
|> line(end = [-77.95, -145.93])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
loft([profile001, profile002])
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const [baseProfileEdgeClick] = scene.makeMouseHelpers(621, 292)
const [rect1Crn1] = scene.makeMouseHelpers(592, 283)
const [rect1Crn2] = scene.makeMouseHelpers(797, 268)
await baseProfileEdgeClick()
await toolbar.editSketch()
await page.waitForTimeout(600)
await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 562, y: 172 }, 15)
await toolbar.rectangleBtn.click()
await page.waitForTimeout(100)
await rect1Crn1()
await editor.expectEditor.toContain(
`profile003 = startProfile(sketch001, at = [50.72, -18.19])`
)
await rect1Crn2()
await editor.expectEditor.toContain(
`angledLine(angle = 0, length = 113.01, tag = $rectangleSegmentA001)`
)
})
test('Can enter sketch loft edges offsetPlane and continue sketch', async ({
scene,
toolbar,
editor,
page,
homePage,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [34, 42.66])
|> line(end = [102.65, 151.99])
|> line(end = [76, -138.66])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
plane001 = offsetPlane(XZ, offset = 50)
sketch002 = startSketchOn(plane001)
profile002 = startProfile(sketch002, at = [39.43, 172.21])
|> xLine(length = 183.99)
|> line(end = [-77.95, -145.93])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
loft([profile001, profile002])
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
const topProfileEdgeClickCoords = { x: 602, y: 185 } as const
const [topProfileEdgeClick] = scene.makeMouseHelpers(
topProfileEdgeClickCoords.x,
topProfileEdgeClickCoords.y
)
const [sideProfileEdgeClick] = scene.makeMouseHelpers(788, 188)
const [rect1Crn1] = scene.makeMouseHelpers(592, 283)
const [rect1Crn2] = scene.makeMouseHelpers(797, 268)
await scene.moveCameraTo(
{ x: 8171, y: -7740, z: 1624 },
{ x: 3302, y: -627, z: 2892 }
)
await topProfileEdgeClick()
await page.waitForTimeout(300)
await toolbar.editSketch()
await page.waitForTimeout(600)
await sideProfileEdgeClick()
await page.waitForTimeout(300)
await scene.expectPixelColor(TEST_COLORS.BLUE, { x: 788, y: 188 }, 15)
await toolbar.rectangleBtn.click()
await page.waitForTimeout(100)
await rect1Crn1()
await editor.expectEditor.toContain(
`profile003 = startProfile(plane001, at = [47.76, -17.13])`
)
await rect1Crn2()
await editor.expectEditor.toContain(
`angledLine(angle = 0, length = 106.42], tag = $rectangleSegmentA001)`
)
await page.waitForTimeout(100)
})
})
// Regression test for https://github.com/KittyCAD/modeling-app/issues/4891
test.describe(`Click based selection don't brick the app when clicked out of range after format using cache`, () => {
test(`Can select a line that reformmed after entering sketch mode`, async ({
context,
page,
scene,
toolbar,
editor,
homePage,
cmdBar,
}) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn(XZ)
|> startProfile(at = [0, 0])
|> line(end = [3.14, 3.14])
|> arc(
interiorAbsolute = [1, 2],
endAbsolute = [4, 2]
)`
)
})
await homePage.goToModelingScene()
await scene.settled(cmdBar)
const formattedArc = `arc(interiorAbsolute = [1, 2], endAbsolute = [4, 2])`
await test.step(`format the code`, async () => {
// doesn't contain condensed version
await editor.expectEditor.not.toContain(formattedArc)
// click the code to enter sketch mode
await page.getByText(`arc`).click()
// Format the code.
await page.locator('#code-pane button:first-child').click()
await page.locator('button:has-text("Format code")').click()
})
await test.step(`Ensure the code reformatted`, async () => {
await editor.expectEditor.toContain(formattedArc)
})
const [arcClick, arcHover] = scene.makeMouseHelpers(699, 337)
await test.step('Ensure we can hover the arc', async () => {
await arcHover()
// Check that the code is highlighted
await editor.expectState({
activeLines: ['sketch001=startSketchOn(XZ)'],
diagnostics: [],
highlightedCode: 'arc(interiorAbsolute = [1, 2], endAbsolute = [4, 2])',
})
})
await test.step('reset the selection', async () => {
// Move the mouse out of the way
await page.mouse.move(655, 337)
await editor.expectState({
activeLines: ['sketch001=startSketchOn(XZ)'],
diagnostics: [],
highlightedCode: '',
})
})
await test.step('Ensure we can click the arc', async () => {
await arcClick()
// Check that the code is highlighted
await editor.expectState({
activeLines: [],
diagnostics: [],
highlightedCode: 'arc(interiorAbsolute = [1, 2], endAbsolute = [4, 2])',
})
})
})
})
// Regression test for https://github.com/KittyCAD/modeling-app/issues/4372
test.describe('Redirecting to home page and back to the original file should clear sketch DOM elements', () => {
test('Can redirect to home page and back to original file and have a cleared DOM', async ({
context,
page,
scene,
toolbar,
editor,
homePage,
cmdBar,
}) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
'persistCode',
` sketch001 = startSketchOn(XZ)
|> startProfile(at = [256.85, 14.41])
|> line(endAbsolute = [0, 211.07])
`
)
})
await homePage.goToModelingScene()
await scene.settled(cmdBar)
const [objClick] = scene.makeMouseHelpers(634, 274)
await objClick()
// Enter sketch mode
await toolbar.editSketch()
await expect(page.getByText('323.49')).toBeVisible()
// Open navigation side bar
await page.getByTestId('project-sidebar-toggle').click()
const goToHome = page.getByRole('button', {
name: 'Go to Home',
})
await goToHome.click()
await homePage.openProject('testDefault')
await expect(page.getByText('323.49')).not.toBeVisible()
})
test('Straight line snapping to previous tangent', async ({
page,
homePage,
toolbar,
scene,
cmdBar,
context,
editor,
}) => {
await context.addInitScript(() => {
localStorage.setItem('persistCode', `@settings(defaultLengthUnit = mm)`)
})
const viewportSize = { width: 1200, height: 900 }
await page.setBodyDimensions(viewportSize)
await homePage.goToModelingScene()
// wait until scene is ready to be interacted with
await scene.connectionEstablished()
await scene.settled(cmdBar)
await page.getByRole('button', { name: 'Start Sketch' }).click()
// select an axis plane
await page.mouse.click(700, 200)
// Needed as we don't yet have a way to get a signal from the engine that the camera has animated to the sketch plane
await page.waitForTimeout(3000)
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
const { click00r } = getMovementUtils({ center, page })
// Draw line
await click00r(0, 0)
await click00r(200, -200)
// Draw arc
await toolbar.selectTangentialArc()
await click00r(0, 0)
await click00r(100, 100)
// Switch back to line
await toolbar.selectLine()
await click00r(0, 0)
await click00r(-100, 100)
// Draw a 3 point arc
await toolbar.selectThreePointArc()
await click00r(0, 0)
await click00r(0, 100)
await click00r(100, 0)
// draw a line to opposite tangent direction of previous arc
await toolbar.selectLine()
await click00r(0, 0)
await click00r(-200, 200)
await editor.expectEditor.toContain(
`@settings(defaultLengthUnit = mm)
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [0, 0])
|> line(end = [191.39, 191.39])
|> tangentialArc(endAbsolute = [287.08, 95.69], tag = $seg01)
|> angledLine(angle = tangentToEnd(seg01), length = 135.34)
|> arc(interiorAbsolute = [191.39, -95.69], endAbsolute = [287.08, -95.69], tag = $seg02)
|> angledLine(angle = tangentToEnd(seg02) + turns::HALF_TURN, length = 270.67)
`.replaceAll('\n', '')
)
})
})
test.describe('manual edits during sketch mode', () => {
test('Can edit sketch through feature tree with variable modifications', async ({
page,
context,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `myVar1 = 5
myVar2 = 6
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [106.68, 89.77])
|> line(end = [132.34, 157.8])
|> line(end = [67.65, -460.55], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = 500)
sketch002 = startSketchOn(extrude001, face = seg01)
profile002 = startProfile(sketch002, at = [83.39, 329.15])
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|> angledLine(length = 156.54, angle = -28)
|> angledLine(
angle = -151,
length = 116.27,
)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfile(sketch002, at = [-201.08, 254.17])
|> line(end = [103.55, 33.32])
|> line(end = [48.8, -153.54])`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await test.step('Open feature tree and edit second sketch', async () => {
await toolbar.openFeatureTreePane()
const sketchButton = await toolbar.getFeatureTreeOperation('Sketch', 1)
await sketchButton.dblclick()
await page.waitForTimeout(700) // Wait for engine animation
})
await test.step('Add new variable and wait for re-execution', async () => {
await page.waitForTimeout(500) // wait for deferred execution
await editor.replaceCode('myVar2 = 6', 'myVar2 = 6\nmyVar3 = 7')
await page.waitForTimeout(2000) // wait for deferred execution
})
const handle1Location = { x: 843, y: 235 }
await test.step('Edit sketch by dragging handle', async () => {
await page.waitForTimeout(500)
await expect
.poll(
async () => {
await editor.expectEditor.toContain('length = 156.54, angle = -28')
await page.mouse.move(handle1Location.x, handle1Location.y)
await page.mouse.down()
await page.mouse.move(
handle1Location.x + 50,
handle1Location.y + 50,
{
steps: 5,
}
)
await page.mouse.up()
await editor.expectEditor.toContain('length = 231.59, angle = -34')
return true
},
{ timeout: 10_000 }
)
.toBeTruthy()
})
await test.step('Delete variables and wait for re-execution', async () => {
await page.waitForTimeout(500)
await editor.replaceCode('myVar3 = 7', '')
await page.waitForTimeout(50)
await editor.replaceCode('myVar2 = 6', '')
await page.waitForTimeout(2000) // Wait for deferred execution
})
const handle2Location = { x: 872, y: 273 }
await test.step('Edit sketch again', async () => {
await editor.expectEditor.toContain('length = 231.59, angle = -34')
await page.waitForTimeout(500)
await expect
.poll(
async () => {
await page.mouse.move(handle2Location.x, handle2Location.y)
await page.mouse.down()
await page.mouse.move(handle2Location.x, handle2Location.y - 50, {
steps: 5,
})
await page.mouse.up()
await editor.expectEditor.toContain('length = 167.36, angle = -14')
return true
},
{ timeout: 10_000 }
)
.toBeTruthy()
})
await test.step('add whole other sketch before current sketch', async () => {
await page.waitForTimeout(500)
await editor.replaceCode(
`myVar1 = 5`,
`myVar1 = 5
sketch003 = startSketchOn(XY)
profile004 = circle(sketch003, center = [143.91, 136.89], radius = 71.63)`
)
await page.waitForTimeout(2000) // Wait for deferred execution
})
const handle3Location = { x: 844, y: 212 }
await test.step('edit sketch again', async () => {
await page.waitForTimeout(500) // Wait for deferred execution
await expect
.poll(
async () => {
await editor.expectEditor.toContain('length = 167.36, angle = -14')
await page.mouse.move(handle3Location.x, handle3Location.y)
await page.mouse.down()
await page.mouse.move(handle3Location.x, handle3Location.y + 110, {
steps: 5,
})
await page.mouse.up()
await editor.expectEditor.toContain('length = 219.2, angle = -56')
return true
},
{ timeout: 10_000 }
)
.toBeTruthy()
})
// exit sketch and assert whole code
await test.step('Exit sketch and assert code', async () => {
await toolbar.exitSketch()
await editor.expectEditor.toContain(
`myVar1 = 5
sketch003 = startSketchOn(XY)
profile004 = circle(sketch003, center = [143.91, 136.89], radius = 71.63)
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [106.68, 89.77])
|> line(end = [132.34, 157.8])
|> line(end = [67.65, -460.55], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = 500)
sketch002 = startSketchOn(extrude001, face = seg01)
profile002 = startProfile(sketch002, at = [83.39, 329.15])
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|> angledLine(length = 219.2, angle = -56)
|> angledLine(angle = -151, length = 116.27)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfile(sketch002, at = [-201.08, 254.17])
|> line(end = [103.55, 33.32])
|> line(end = [48.8, -153.54])
`,
{ shouldNormalise: true }
)
await editor.expectState({
activeLines: [],
diagnostics: [],
highlightedCode: '',
})
})
})
test('Will exit out of sketch mode for some incompatible edits', async ({
page,
context,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `myVar1 = 5
myVar2 = 6
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [106.68, 89.77])
|> line(end = [132.34, 157.8])
|> line(end = [67.65, -460.55], tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(profile001, length = 500)
sketch002 = startSketchOn(extrude001, face = seg01)
profile002 = startProfile(sketch002, at = [83.39, 329.15])
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|> angledLine(length = 156.54, angle = -28)
|> angledLine(
angle = -151,
length = 116.27,
)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = startProfile(sketch002, at = [-201.08, 254.17])
|> line(end = [103.55, 33.32])
|> line(end = [48.8, -153.54])`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
const expectSketchOriginToBeDrawn = async () => {
await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 672, y: 193 }, 15)
}
await test.step('Open feature tree and edit second sketch', async () => {
await toolbar.openFeatureTreePane()
const sketchButton = await toolbar.getFeatureTreeOperation('Sketch', 1)
await sketchButton.dblclick()
await page.waitForTimeout(700) // Wait for engine animation
await expectSketchOriginToBeDrawn()
})
await test.step('rename variable of current sketch, sketch002 to changeSketchNamePartWayThrough', async () => {
await editor.replaceCode('sketch002', 'changeSketchNamePartWayThrough')
await page.waitForTimeout(100)
// three times to rename the declaration and it's use
await editor.replaceCode('sketch002', 'changeSketchNamePartWayThrough')
await page.waitForTimeout(100)
await editor.replaceCode('sketch002', 'changeSketchNamePartWayThrough')
await expect(
page.getByText('Unable to maintain sketch mode')
).toBeVisible()
})
})
test('empty draft sketch is cleaned up properly', async ({
scene,
toolbar,
cmdBar,
page,
homePage,
}) => {
// This is the sketch used in the original report, but any sketch would work
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`yRel002 = 200
lStraight = -200
yRel001 = -lStraight
length001 = lStraight
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [-102.72, 237.44])
|> yLine(length = lStraight)
|> tangentialArc(endAbsolute = [118.9, 23.57])
|> line(end = [-17.64, yRel002])
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
// Ensure start sketch button is enabled
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
// Start a new sketch
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
await toolbar.startSketchPlaneSelection()
await selectXZPlane()
await page.waitForTimeout(2000) // wait for engine animation
// Switch to a different tool (circle)
await toolbar.circleBtn.click()
await expect(toolbar.circleBtn).toHaveAttribute('aria-pressed', 'true')
// Exit the empty sketch
await page.getByRole('button', { name: 'Exit Sketch' }).click()
// Ensure the feature tree now shows only one sketch
await toolbar.openFeatureTreePane()
await expect(
toolbar.featureTreePane.getByRole('button', { name: 'Sketch' })
).toHaveCount(1)
await toolbar.closeFeatureTreePane()
// Open the first sketch from the feature tree (the existing sketch)
await (await toolbar.getFeatureTreeOperation('Sketch', 0)).dblclick()
// timeout is a bit longer because when the bug happened, it did go into sketch mode for a split second, but returned
// automatically, we want to make sure it stays there.
await page.waitForTimeout(2000)
// Validate we are in sketch mode (Exit Sketch button visible)
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
})
// Ensure feature tree is not showing previous file's content when switching to a file with KCL errors.
test('Feature tree shows correct sketch count per file', async ({
context,
homePage,
scene,
toolbar,
cmdBar,
page,
}) => {
const u = await getUtils(page)
// Setup project with files.
const GOOD_KCL = `sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [220.81, 253.8])
|> line(end = [132.84, -151.31])
|> line(end = [25.51, 167.15])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch002 = startSketchOn(XZ)
profile002 = startProfile(sketch002, at = [158.35, -70.82])
|> line(end = [73.9, -152.19])
|> line(end = [85.33, 135.48])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`
const ERROR_KCL = `sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [127.56, 179.02])
|> line(end = [132.84, -112.6])
|> line(end = [85.33, 234.01])
|> line(enfd = [-137.23, -54.55])`
await context.folderSetupFn(async (dir) => {
const projectDir = path.join(dir, 'multi-file-sketch-test')
await fs.mkdir(projectDir, { recursive: true })
await Promise.all([
fs.writeFile(path.join(projectDir, 'good.kcl'), GOOD_KCL, 'utf-8'),
fs.writeFile(path.join(projectDir, 'error.kcl'), ERROR_KCL, 'utf-8'),
])
})
await page.setBodyDimensions({ width: 1000, height: 800 })
await homePage.openProject('multi-file-sketch-test')
await scene.connectionEstablished()
await u.closeDebugPanel()
await toolbar.openFeatureTreePane()
await toolbar.openPane('files')
await toolbar.openFile('good.kcl')
await expect(
toolbar.featureTreePane.getByRole('button', { name: 'Sketch' })
).toHaveCount(2)
await toolbar.openFile('error.kcl')
await expect(
toolbar.featureTreePane.getByRole('button', { name: 'Sketch' })
).toHaveCount(0)
})
test('adding a syntax error, recovers after fixing', async ({
page,
homePage,
context,
scene,
editor,
toolbar,
cmdBar,
}) => {
const file = await fs.readFile(
path.resolve(
__dirname,
'../../',
'./rust/kcl-lib/e2e/executor/inputs/e2e-can-sketch-on-chamfer.kcl'
),
'utf-8'
)
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
const [objClick] = scene.makeMouseHelpers(600, 250)
const arrowHeadLocation = { x: 706, y: 129 } as const
const arrowHeadWhite = TEST_COLORS.WHITE
const backgroundGray: [number, number, number] = [28, 28, 28]
const verifyArrowHeadColor = async (c: [number, number, number]) =>
scene.expectPixelColor(c, arrowHeadLocation, 15)
// wait for scene to load
await scene.settled(cmdBar)
await test.step('check chamfer selection changes cursor position', async () => {
await expect(async () => {
// sometimes initial click doesn't register
await objClick()
await editor.expectActiveLinesToBe([
'|> startProfile(at = [75.8, 317.2]) // [$startCapTag, $EndCapTag]',
])
}).toPass({ timeout: 15_000, intervals: [500] })
})
await test.step('enter sketch and sanity check segments have been drawn', async () => {
await toolbar.editSketch()
// this checks sketch segments have been drawn
await verifyArrowHeadColor(arrowHeadWhite)
})
await test.step('Make typo and check the segments have Disappeared and there is a syntax error', async () => {
await editor.replaceCode(
'line(endAbsolute = [pro',
'badBadBadFn(endAbsolute = [pro'
)
await editor.expectState({
activeLines: [],
diagnostics: ['`badBadBadFn`isnotdefined'],
highlightedCode: '',
})
await expect(
page.getByText(
"Error in kcl script, sketch cannot be drawn until it's fixed"
)
).toBeVisible()
// this checks sketch segments have failed to be drawn
await verifyArrowHeadColor(backgroundGray)
})
await test.step('', async () => {
await editor.replaceCode(
'badBadBadFn(endAbsolute = [pro',
'line(endAbsolute = [pro'
)
await editor.expectState({
activeLines: [],
diagnostics: [],
highlightedCode: '',
})
// this checks sketch segments have been drawn
await verifyArrowHeadColor(arrowHeadWhite)
})
await test.step('make a change to the code and expect pixel color to change', async () => {
// defends against a regression where sketch would duplicate in the scene
// https://github.com/KittyCAD/modeling-app/issues/6345
await editor.replaceCode(
'startProfile(at = [75.8, 317.2',
'startProfile(at = [75.8, 217.2'
)
// expect not white anymore
await scene.expectPixelColorNotToBe(
TEST_COLORS.WHITE,
arrowHeadLocation,
15
)
})
})
})