Compare commits
	
		
			1 Commits
		
	
	
		
			v0.17.3
			...
			achalmers/
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 225dc56efb | 
| @ -1,3 +1,3 @@ | ||||
| [codespell] | ||||
| ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast | ||||
| skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md | ||||
| ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey | ||||
| skip: **/target,node_modules,build,**/Cargo.lock | ||||
|  | ||||
							
								
								
									
										50
									
								
								.github/workflows/cargo-build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,50 @@ | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - '**.rs' | ||||
|       - '**/Cargo.toml' | ||||
|       - '**/Cargo.lock' | ||||
|       - '**/rust-toolchain.toml' | ||||
|       - .github/workflows/cargo-build.yml | ||||
|   pull_request: | ||||
|     paths: | ||||
|       - '**.rs' | ||||
|       - '**/Cargo.toml' | ||||
|       - '**/Cargo.lock' | ||||
|       - '**/rust-toolchain.toml' | ||||
|       - .github/workflows/cargo-build.yml | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | ||||
|   cancel-in-progress: true | ||||
| name: cargo build | ||||
| jobs: | ||||
|   cargobuild: | ||||
|     name: cargo build | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         dir: ['src/wasm-lib'] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Install latest rust | ||||
|         uses: actions-rs/toolchain@v1 | ||||
|         with: | ||||
|             toolchain: stable | ||||
|             override: true | ||||
|  | ||||
|       - name: install dependencies | ||||
|         if: matrix.dir ==  'src-tauri' | ||||
|         run: | | ||||
|           sudo apt-get update | ||||
|           sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf | ||||
|       - name: Rust Cache | ||||
|         uses: Swatinem/rust-cache@v2.6.1 | ||||
|  | ||||
|       - name: Run cargo build | ||||
|         run: | | ||||
|           cd "${{ matrix.dir }}" | ||||
|           cargo build --all | ||||
|         shell: bash | ||||
							
								
								
									
										6
									
								
								.github/workflows/cargo-clippy.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -9,6 +9,12 @@ on: | ||||
|       - '**.rs' | ||||
|       - .github/workflows/cargo-clippy.yml | ||||
|   pull_request: | ||||
|     paths: | ||||
|       - '**/Cargo.toml' | ||||
|       - '**/Cargo.lock' | ||||
|       - '**/rust-toolchain.toml' | ||||
|       - '**.rs' | ||||
|       - .github/workflows/cargo-build.yml | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| @ -7,23 +7,23 @@ on: | ||||
|       - '**/Cargo.toml' | ||||
|       - '**/Cargo.lock' | ||||
|       - '**/rust-toolchain.toml' | ||||
|       - .github/workflows/cargo-bench.yml | ||||
|       - .github/workflows/cargo-criterion.yml | ||||
|   pull_request: | ||||
|     paths: | ||||
|       - '**.rs' | ||||
|       - '**/Cargo.toml' | ||||
|       - '**/Cargo.lock' | ||||
|       - '**/rust-toolchain.toml' | ||||
|       - .github/workflows/cargo-bench.yml | ||||
|       - .github/workflows/cargo-criterion.yml | ||||
|   workflow_dispatch: | ||||
| permissions: read-all | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | ||||
|   cancel-in-progress: true | ||||
| name: cargo bench | ||||
| name: cargo criterion | ||||
| jobs: | ||||
|   cargo-bench: | ||||
|     name: Benchmark with iai | ||||
|   cargocriterion: | ||||
|     name: cargo criterion | ||||
|     runs-on: ubuntu-latest-8-cores | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
| @ -31,12 +31,10 @@ jobs: | ||||
|       - name: Install dependencies | ||||
|         run: | | ||||
|           cargo install cargo-criterion | ||||
|           sudo apt update | ||||
|           sudo apt install -y valgrind | ||||
|       - name: Rust Cache | ||||
|         uses: Swatinem/rust-cache@v2.6.1 | ||||
|       - name: Benchmark kcl library | ||||
|         shell: bash | ||||
|         run: |- | ||||
|           cd src/wasm-lib/kcl; cargo bench -- iai | ||||
|           cd src/wasm-lib/kcl; cargo criterion | ||||
| 
 | ||||
							
								
								
									
										36
									
								
								.github/workflows/check-exampleKcl.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -1,36 +0,0 @@ | ||||
| name: Check Onboarding KCL | ||||
|  | ||||
| on: | ||||
|   pull_request: | ||||
|     types: [opened, synchronize] | ||||
|     paths: | ||||
|       - 'src/lib/exampleKcl.ts' | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|   issues: write | ||||
|   pull-requests: write | ||||
|  | ||||
| jobs: | ||||
|   comment: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Comment on PR | ||||
|         uses: actions/github-script@v7 | ||||
|         with: | ||||
|           script: | | ||||
|             const message = '`src/lib/exampleKcl.ts` has been updated in this PR, please review and update the `src/routes/onboarding`, if needed.'; | ||||
|             const issue_number = context.payload.pull_request.number; | ||||
|             const owner = context.repo.owner; | ||||
|             const repo = context.repo.repo; | ||||
|  | ||||
|             // Post a comment on the PR | ||||
|             await github.rest.issues.createComment({ | ||||
|               owner, | ||||
|               repo, | ||||
|               issue_number, | ||||
|               body: message, | ||||
|             }); | ||||
							
								
								
									
										4
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -9,10 +9,6 @@ concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| permissions: | ||||
|   contents: write | ||||
|   pull-requests: write | ||||
|  | ||||
| jobs: | ||||
|   playwright-ubuntu: | ||||
|     timeout-minutes: 60 | ||||
|  | ||||
| @ -281,7 +281,7 @@ https://github.com/KittyCAD/modeling-app/assets/29681384/6f5e8e85-1003-4fd9-be7f | ||||
| <details> | ||||
|  | ||||
| <summary> | ||||
| PS: for the debug panel, the following JSON is useful for snapping the camera | ||||
| Ps for the debug panel, the following JSON is useful for snapping the camera | ||||
| </summary> | ||||
|  | ||||
| ```JSON | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the absolute value of a number. | ||||
| abs(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the arccosine of a number (in radians). | ||||
| acos(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the arcsine of a number (in radians). | ||||
| asin(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the arctangent of a number (in radians). | ||||
| atan(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the smallest integer greater than or equal to a number. | ||||
| ceil(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| --- | ||||
| title: "cos" | ||||
| excerpt: "Computes the cosine of a number (in radians)." | ||||
| excerpt: "Computes the sine of a number (in radians)." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Computes the cosine of a number (in radians). | ||||
| Computes the sine of a number (in radians). | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the cosine of a number (in radians). | ||||
| cos(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Return the value of Euler’s number `e`. | ||||
| e() -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the largest integer less than or equal to a number. | ||||
| floor(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -30,12 +30,10 @@ layout: manual | ||||
| * [`extrude`](kcl/extrude) | ||||
| * [`fillet`](kcl/fillet) | ||||
| * [`floor`](kcl/floor) | ||||
| * [`getEdge`](kcl/getEdge) | ||||
| * [`getExtrudeWallTransform`](kcl/getExtrudeWallTransform) | ||||
| * [`getNextAdjacentEdge`](kcl/getNextAdjacentEdge) | ||||
| * [`getOppositeEdge`](kcl/getOppositeEdge) | ||||
| * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) | ||||
| * [`helix`](kcl/helix) | ||||
| * [`hole`](kcl/hole) | ||||
| * [`import`](kcl/import) | ||||
| * [`lastSegX`](kcl/lastSegX) | ||||
| @ -57,7 +55,6 @@ layout: manual | ||||
| * [`patternLinear3d`](kcl/patternLinear3d) | ||||
| * [`pi`](kcl/pi) | ||||
| * [`pow`](kcl/pow) | ||||
| * [`revolve`](kcl/revolve) | ||||
| * [`segAng`](kcl/segAng) | ||||
| * [`segEndX`](kcl/segEndX) | ||||
| * [`segEndY`](kcl/segEndY) | ||||
|  | ||||
| @ -12,10 +12,6 @@ Returns the angle of the given leg for x. | ||||
| legAngX(hypotenuse: number, leg: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `utilities` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Returns the angle of the given leg for y. | ||||
| legAngY(hypotenuse: number, leg: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `utilities` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Returns the length of the given leg. | ||||
| legLen(hypotenuse: number, leg: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `utilities` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the natural logarithm of the number. | ||||
| ln(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ The result might not be correctly rounded owing to implementation details; `log2 | ||||
| log(num: number, base: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the base 10 logarithm of the number. | ||||
| log10(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the base 2 logarithm of the number. | ||||
| log2(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the maximum of the given arguments. | ||||
| max(args: [number]) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the minimum of the given arguments. | ||||
| min(args: [number]) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Return the value of `pi`. Archimedes’ constant (π). | ||||
| pi() -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the number to a power. | ||||
| pow(num: number, pow: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the sine of a number (in radians). | ||||
| sin(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Computes the square root of a number. | ||||
| sqrt(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
							
								
								
									
										4675
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
							
						
						| @ -12,10 +12,6 @@ Computes the tangent of a number (in radians). | ||||
| tan(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Return the value of `tau`. The full circle constant (τ). Equal to 2π. | ||||
| tau() -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Converts a number from radians to degrees. | ||||
| toDegrees(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| @ -12,10 +12,6 @@ Converts a number from degrees to radians. | ||||
| toRadians(num: number) -> number | ||||
| ``` | ||||
|  | ||||
| ### Tags | ||||
|  | ||||
| * `math` | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```js | ||||
|  | ||||
| Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 193 KiB | 
| Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 193 KiB | 
| Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 193 KiB | 
| Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 259 KiB | 
| Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 220 KiB | 
| Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 220 KiB | 
| Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 220 KiB | 
| Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 193 KiB | 
| Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 221 KiB | 
| Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 221 KiB | 
| @ -1,11 +1,10 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { secrets } from './secrets' | ||||
| import { getUtils } from './test-utils' | ||||
| import waitOn from 'wait-on' | ||||
| import { Themes } from '../../src/lib/theme' | ||||
| import { initialSettings } from '../../src/lib/settings/initialSettings' | ||||
| import { roundOff } from 'lib/utils' | ||||
| import { basicStorageState } from './storageStates' | ||||
| import * as TOML from '@iarna/toml' | ||||
| import { SaveSettingsPayload } from 'lib/settings/settingsTypes' | ||||
| import { Themes } from 'lib/theme' | ||||
|  | ||||
| /* | ||||
| debug helper: unfortunately we do rely on exact coord mouse clicks in a few places | ||||
| @ -21,8 +20,6 @@ const commonPoints = { | ||||
|   startAt: '[9.06, -12.22]', | ||||
|   num1: 9.14, | ||||
|   num2: 18.2, | ||||
|   // num1: 9.64, | ||||
|   // num2: 19.19, | ||||
| } | ||||
|  | ||||
| test.beforeEach(async ({ context, page }) => { | ||||
| @ -31,14 +28,31 @@ test.beforeEach(async ({ context, page }) => { | ||||
|     resources: ['tcp:3000'], | ||||
|     timeout: 5000, | ||||
|   }) | ||||
|  | ||||
|   await context.addInitScript(async (token) => { | ||||
|     localStorage.setItem('TOKEN_PERSIST_KEY', token) | ||||
|     localStorage.setItem('persistCode', ``) | ||||
|     localStorage.setItem( | ||||
|       'SETTINGS_PERSIST_KEY', | ||||
|       JSON.stringify({ | ||||
|         baseUnit: 'in', | ||||
|         cameraControls: 'KittyCAD', | ||||
|         defaultDirectory: '', | ||||
|         defaultProjectName: 'project-$nnn', | ||||
|         onboardingStatus: 'dismissed', | ||||
|         showDebugPanel: true, | ||||
|         textWrapping: 'On', | ||||
|         theme: 'system', | ||||
|         unitSystem: 'imperial', | ||||
|       }) | ||||
|     ) | ||||
|   }, secrets.token) | ||||
|   // kill animations, speeds up tests and reduced flakiness | ||||
|   await page.emulateMedia({ reducedMotion: 'reduce' }) | ||||
| }) | ||||
|  | ||||
| test.setTimeout(60000) | ||||
|  | ||||
| test('Basic sketch', async ({ page, context }) => { | ||||
| test('Basic sketch', async ({ page }) => { | ||||
|   const u = getUtils(page) | ||||
|   await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|   const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
| @ -62,7 +76,6 @@ test('Basic sketch', async ({ page, context }) => { | ||||
|   await expect(page.locator('.cm-content')).toHaveText( | ||||
|     `const part001 = startSketchOn('-XZ')` | ||||
|   ) | ||||
|   await u.closeDebugPanel() | ||||
|  | ||||
|   await page.waitForTimeout(300) // TODO detect animation ending, or disable animation | ||||
|  | ||||
| @ -73,6 +86,7 @@ test('Basic sketch', async ({ page, context }) => { | ||||
|   |> startProfileAt(${commonPoints.startAt}, %)`) | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   await u.closeDebugPanel() | ||||
|   await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
| @ -458,18 +472,8 @@ test('Auto complete works', async ({ page }) => { | ||||
|   const u = getUtils(page) | ||||
|   // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|   await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|   const lspStartPromise = page.waitForEvent('console', async (message) => { | ||||
|     // it would be better to wait for a message that the kcl lsp has started by looking for the message  message.text().includes('[lsp] [window/logMessage]') | ||||
|     // but that doesn't seem to make it to the console for macos/safari :( | ||||
|     if (message.text().includes('start kcl lsp')) { | ||||
|       await new Promise((resolve) => setTimeout(resolve, 200)) | ||||
|       return true | ||||
|     } | ||||
|     return false | ||||
|   }) | ||||
|   await page.goto('/') | ||||
|   await u.waitForAuthSkipAppStart() | ||||
|   await lspStartPromise | ||||
|  | ||||
|   // this test might be brittle as we add and remove functions | ||||
|   // but should also be easy to update. | ||||
| @ -513,133 +517,96 @@ test('Auto complete works', async ({ page }) => { | ||||
| }) | ||||
|  | ||||
| // Stored settings validation test | ||||
| test.describe('Settings persistence and validation tests', () => { | ||||
|   // Override test setup | ||||
| test('Stored settings are validated and fall back to defaults', async ({ | ||||
|   page, | ||||
|   context, | ||||
| }) => { | ||||
|   // Override beforeEach test setup | ||||
|   // with corrupted settings | ||||
|   const storageState = structuredClone(basicStorageState) | ||||
|   const s = TOML.parse(storageState.origins[0].localStorage[2].value) as { | ||||
|     settings: SaveSettingsPayload | ||||
|   } | ||||
|   s.settings.app.theme = Themes.Dark | ||||
|   s.settings.app.projectDirectory = 123 as any | ||||
|   s.settings.modeling.defaultUnit = 'invalid' as any | ||||
|   s.settings.modeling.mouseControls = `() => alert('hack the planet')` as any | ||||
|   s.settings.projects.defaultProjectName = false as any | ||||
|   storageState.origins[0].localStorage[2].value = TOML.stringify(s) | ||||
|   await context.addInitScript(async () => { | ||||
|     const storedSettings = JSON.parse( | ||||
|       localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}' | ||||
|     ) | ||||
|  | ||||
|   test.use({ storageState }) | ||||
|     // Corrupt the settings | ||||
|     storedSettings.baseUnit = 'invalid' | ||||
|     storedSettings.cameraControls = `() => alert('hack the planet')` | ||||
|     storedSettings.defaultDirectory = 123 | ||||
|     storedSettings.defaultProjectName = false | ||||
|  | ||||
|   test('Stored settings are validated and fall back to defaults', async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     const u = getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.goto('/') | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     // Check the settings were reset | ||||
|     const storedSettings = TOML.parse( | ||||
|       await page.evaluate(() => localStorage.getItem('/user.toml') || '{}') | ||||
|     ) as { settings: SaveSettingsPayload } | ||||
|  | ||||
|     expect(storedSettings.settings.app?.theme).toBe('dark') | ||||
|  | ||||
|     // Check that the invalid settings were removed | ||||
|     expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined) | ||||
|     expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined) | ||||
|     expect(storedSettings.settings.app?.projectDirectory).toBe(undefined) | ||||
|     expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined) | ||||
|     localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings)) | ||||
|   }) | ||||
|  | ||||
|   test('Project settings can be set and override user settings', async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     const u = getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.goto('/') | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|   await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|   await page.goto('/', { waitUntil: 'domcontentloaded' }) | ||||
|  | ||||
|     // Open the settings modal with the browser keyboard shortcut | ||||
|     await page.keyboard.press('Meta+Shift+,') | ||||
|   // Check the toast appeared | ||||
|   await expect( | ||||
|     page.getByText(`Error validating persisted settings:`, { | ||||
|       exact: false, | ||||
|     }) | ||||
|   ).toBeVisible() | ||||
|  | ||||
|     await expect( | ||||
|       page.getByRole('heading', { name: 'Settings', exact: true }) | ||||
|     ).toBeVisible() | ||||
|     await page | ||||
|       .locator('select[name="app-theme"]') | ||||
|       .selectOption({ value: 'light' }) | ||||
|  | ||||
|     // Verify the toast appeared | ||||
|     await expect( | ||||
|       page.getByText(`Set theme to "light" for this project`) | ||||
|     ).toBeVisible() | ||||
|     // Check that the theme changed | ||||
|     await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) | ||||
|  | ||||
|     // Check that the user setting was not changed | ||||
|     await page.getByRole('radio', { name: 'User' }).click() | ||||
|     await expect(page.locator('select[name="app-theme"]')).toHaveValue('dark') | ||||
|  | ||||
|     // Roll back to default "system" theme | ||||
|     await page | ||||
|       .getByText( | ||||
|         'themeRoll back themeRoll back to match defaultThe overall appearance of the appl' | ||||
|       ) | ||||
|       .hover() | ||||
|     await page | ||||
|       .getByRole('button', { | ||||
|         name: 'Roll back theme ; Has tooltip: Roll back to match default', | ||||
|       }) | ||||
|       .click() | ||||
|     await expect(page.locator('select[name="app-theme"]')).toHaveValue('system') | ||||
|  | ||||
|     // Check that the project setting did not change | ||||
|     await page.getByRole('radio', { name: 'Project' }).click() | ||||
|     await expect(page.locator('select[name="app-theme"]')).toHaveValue('light') | ||||
|   }) | ||||
|   // Check the settings were reset | ||||
|   const storedSettings = JSON.parse( | ||||
|     await page.evaluate( | ||||
|       () => localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}' | ||||
|     ) | ||||
|   ) | ||||
|   await expect(storedSettings.baseUnit).toBe(initialSettings.baseUnit) | ||||
|   await expect(storedSettings.cameraControls).toBe( | ||||
|     initialSettings.cameraControls | ||||
|   ) | ||||
|   await expect(storedSettings.defaultDirectory).toBe( | ||||
|     initialSettings.defaultDirectory | ||||
|   ) | ||||
|   await expect(storedSettings.defaultProjectName).toBe( | ||||
|     initialSettings.defaultProjectName | ||||
|   ) | ||||
| }) | ||||
|  | ||||
| // Onboarding tests | ||||
| test.describe('Onboarding tests', () => { | ||||
|   // Override test setup | ||||
|   const storageState = structuredClone(basicStorageState) | ||||
|   const s = TOML.parse(storageState.origins[0].localStorage[2].value) as { | ||||
|     settings: SaveSettingsPayload | ||||
|   } | ||||
|   s.settings.app.onboardingStatus = '/export' | ||||
|   storageState.origins[0].localStorage[2].value = TOML.stringify(s) | ||||
|   test.use({ storageState }) | ||||
| test('Onboarding redirects and code updating', async ({ page, context }) => { | ||||
|   const u = getUtils(page) | ||||
|  | ||||
|   test('Onboarding redirects and code updating', async ({ page, context }) => { | ||||
|     const u = getUtils(page) | ||||
|   // Override beforeEach test setup | ||||
|   await context.addInitScript(async () => { | ||||
|     // Give some initial code, so we can test that it's cleared | ||||
|     localStorage.setItem('persistCode', 'const sigmaAllow = 15000') | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.goto('/') | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     // Test that the redirect happened | ||||
|     await expect(page.url().split(':3000').slice(-1)[0]).toBe( | ||||
|       `/file/%2Fbrowser%2Fmain.kcl/onboarding/export` | ||||
|     const storedSettings = JSON.parse( | ||||
|       localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}' | ||||
|     ) | ||||
|  | ||||
|     // Test that you come back to this page when you refresh | ||||
|     await page.reload() | ||||
|     await expect(page.url().split(':3000').slice(-1)[0]).toBe( | ||||
|       `/file/%2Fbrowser%2Fmain.kcl/onboarding/export` | ||||
|     ) | ||||
|  | ||||
|     // Test that the onboarding pane loaded | ||||
|     const title = page.locator('[data-testid="onboarding-content"]') | ||||
|     await expect(title).toBeAttached() | ||||
|  | ||||
|     // Test that the code changes when you advance to the next step | ||||
|     await page.locator('[data-testid="onboarding-next"]').click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText('') | ||||
|  | ||||
|     // Test that the code is not empty when you click on the next step | ||||
|     await page.locator('[data-testid="onboarding-next"]').click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(/.+/) | ||||
|     storedSettings.onboardingStatus = '/export' | ||||
|     localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings)) | ||||
|   }) | ||||
|  | ||||
|   await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|   await page.goto('/') | ||||
|   await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|   // Test that the redirect happened | ||||
|   await expect(page.url().split(':3000').slice(-1)[0]).toBe( | ||||
|     `/file/new/onboarding/export` | ||||
|   ) | ||||
|  | ||||
|   // Test that you come back to this page when you refresh | ||||
|   await page.reload() | ||||
|   await expect(page.url().split(':3000').slice(-1)[0]).toBe( | ||||
|     `/file/new/onboarding/export` | ||||
|   ) | ||||
|  | ||||
|   // Test that the onboarding pane loaded | ||||
|   const title = page.locator('[data-testid="onboarding-content"]') | ||||
|   await expect(title).toBeAttached() | ||||
|  | ||||
|   // Test that the code changes when you advance to the next step | ||||
|   await page.locator('[data-testid="onboarding-next"]').click() | ||||
|   await expect(page.locator('.cm-content')).toHaveText('') | ||||
|  | ||||
|   // Test that the code is not empty when you click on the next step | ||||
|   await page.locator('[data-testid="onboarding-next"]').click() | ||||
|   await expect(page.locator('.cm-content')).toHaveText(/.+/) | ||||
| }) | ||||
|  | ||||
| test('Selections work on fresh and edited sketch', async ({ page }) => { | ||||
| @ -658,7 +625,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => { | ||||
|   const emptySpaceClick = () => | ||||
|     page.mouse.click(728, 343).then(() => page.waitForTimeout(100)) | ||||
|   const topHorzSegmentClick = () => | ||||
|     page.mouse.click(709, 290).then(() => page.waitForTimeout(100)) | ||||
|     page.mouse.click(709, 289).then(() => page.waitForTimeout(100)) | ||||
|   const bottomHorzSegmentClick = () => | ||||
|     page.mouse.click(767, 396).then(() => page.waitForTimeout(100)) | ||||
|  | ||||
| @ -673,12 +640,13 @@ test('Selections work on fresh and edited sketch', async ({ page }) => { | ||||
|   await page.waitForTimeout(700) // wait for animation | ||||
|  | ||||
|   const startXPx = 600 | ||||
|   await u.closeDebugPanel() | ||||
|   await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toHaveText(`const part001 = startSketchOn('-XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %)`) | ||||
|  | ||||
|   await u.closeDebugPanel() | ||||
|  | ||||
|   await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||
|  | ||||
|   await expect(page.locator('.cm-content')) | ||||
| @ -759,18 +727,13 @@ test('Selections work on fresh and edited sketch', async ({ page }) => { | ||||
|     await emptySpaceClick() | ||||
|  | ||||
|     // select segment in editor than another segment in scene and check there are two cursors | ||||
|     // TODO change this back to shift click in the scene, not cmd click in the editor | ||||
|     await bottomHorzSegmentClick() | ||||
|  | ||||
|     await expect(page.locator('.cm-cursor')).toHaveCount(1) | ||||
|  | ||||
|     await page.keyboard.down(process.platform === 'linux' ? 'Control' : 'Meta') | ||||
|     await page.waitForTimeout(100) | ||||
|     await page.getByText(`  |> line([-${commonPoints.num2}, 0], %)`).click() | ||||
|  | ||||
|     await page.waitForTimeout(300) | ||||
|     await page.keyboard.down('Shift') | ||||
|     await expect(page.locator('.cm-cursor')).toHaveCount(1) | ||||
|     await bottomHorzSegmentClick() | ||||
|     await page.keyboard.up('Shift') | ||||
|     await expect(page.locator('.cm-cursor')).toHaveCount(2) | ||||
|     await page.waitForTimeout(500) | ||||
|     await page.keyboard.up(process.platform === 'linux' ? 'Control' : 'Meta') | ||||
|  | ||||
|     // clear selection by clicking on nothing | ||||
|     await emptySpaceClick() | ||||
| @ -800,134 +763,129 @@ test('Selections work on fresh and edited sketch', async ({ page }) => { | ||||
|   await selectionSequence() | ||||
| }) | ||||
|  | ||||
| test.describe('Command bar tests', () => { | ||||
|   test('Command bar works and can change a setting', async ({ page }) => { | ||||
|     // Brief boilerplate | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.goto('/', { waitUntil: 'domcontentloaded' }) | ||||
| test('Command bar works and can change a setting', async ({ page }) => { | ||||
|   // Brief boilerplate | ||||
|   const u = getUtils(page) | ||||
|   await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|   await page.goto('/') | ||||
|   await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     let cmdSearchBar = page.getByPlaceholder('Search commands') | ||||
|   let cmdSearchBar = page.getByPlaceholder('Search commands') | ||||
|  | ||||
|     // First try opening the command bar and closing it | ||||
|     // It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively | ||||
|     await page | ||||
|       .getByRole('button', { name: 'Ctrl+/' }) | ||||
|       .or(page.getByRole('button', { name: '⌘K' })) | ||||
|       .click() | ||||
|     await expect(cmdSearchBar).toBeVisible() | ||||
|     await page.keyboard.press('Escape') | ||||
|     await expect(cmdSearchBar).not.toBeVisible() | ||||
|   // First try opening the command bar and closing it | ||||
|   // It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively | ||||
|   await page | ||||
|     .getByRole('button', { name: 'Ctrl+/' }) | ||||
|     .or(page.getByRole('button', { name: '⌘K' })) | ||||
|     .click() | ||||
|   await expect(cmdSearchBar).toBeVisible() | ||||
|   await page.keyboard.press('Escape') | ||||
|   await expect(cmdSearchBar).not.toBeVisible() | ||||
|  | ||||
|     // Now try the same, but with the keyboard shortcut, check focus | ||||
|     await page.keyboard.press('Meta+K') | ||||
|     await expect(cmdSearchBar).toBeVisible() | ||||
|     await expect(cmdSearchBar).toBeFocused() | ||||
|   // Now try the same, but with the keyboard shortcut, check focus | ||||
|   await page.keyboard.press('Meta+K') | ||||
|   await expect(cmdSearchBar).toBeVisible() | ||||
|   await expect(cmdSearchBar).toBeFocused() | ||||
|  | ||||
|     // Try typing in the command bar | ||||
|     await page.keyboard.type('theme') | ||||
|     const themeOption = page.getByRole('option', { | ||||
|       name: 'Settings · app · theme', | ||||
|     }) | ||||
|     await expect(themeOption).toBeVisible() | ||||
|     await themeOption.click() | ||||
|     const themeInput = page.getByPlaceholder('Select an option') | ||||
|     await expect(themeInput).toBeVisible() | ||||
|     await expect(themeInput).toBeFocused() | ||||
|     // Select dark theme | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute( | ||||
|       'data-headlessui-state', | ||||
|       'active' | ||||
|   // Try typing in the command bar | ||||
|   await page.keyboard.type('theme') | ||||
|   const themeOption = page.getByRole('option', { name: 'Set Theme' }) | ||||
|   await expect(themeOption).toBeVisible() | ||||
|   await themeOption.click() | ||||
|   const themeInput = page.getByPlaceholder('system') | ||||
|   await expect(themeInput).toBeVisible() | ||||
|   await expect(themeInput).toBeFocused() | ||||
|   // Select dark theme | ||||
|   await page.keyboard.press('ArrowDown') | ||||
|   await page.keyboard.press('ArrowUp') | ||||
|   await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute( | ||||
|     'data-headlessui-state', | ||||
|     'active' | ||||
|   ) | ||||
|   await page.keyboard.press('Enter') | ||||
|  | ||||
|   // Check the toast appeared | ||||
|   await expect(page.getByText(`Set Theme to "${Themes.Dark}"`)).toBeVisible() | ||||
|   // Check that the theme changed | ||||
|   await expect(page.locator('body')).toHaveClass(`body-bg ${Themes.Dark}`) | ||||
| }) | ||||
|  | ||||
| test('Can extrude from the command bar', async ({ page, context }) => { | ||||
|   await context.addInitScript(async (token) => { | ||||
|     localStorage.setItem( | ||||
|       'persistCode', | ||||
|       ` | ||||
|       const distance = sqrt(20) | ||||
|       const part001 = startSketchOn('-XZ') | ||||
|         |> startProfileAt([-6.95, 4.98], %) | ||||
|         |> line([25.1, 0.41], %) | ||||
|         |> line([0.73, -14.93], %) | ||||
|         |> line([-23.44, 0.52], %) | ||||
|         |> close(%) | ||||
|       ` | ||||
|     ) | ||||
|     await page.keyboard.press('Enter') | ||||
|  | ||||
|     // Check the toast appeared | ||||
|     await expect( | ||||
|       page.getByText(`Set theme to "system" for this project`) | ||||
|     ).toBeVisible() | ||||
|     // Check that the theme changed | ||||
|     await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) | ||||
|   }) | ||||
|  | ||||
|   // Override test setup code | ||||
|   const storageState = structuredClone(basicStorageState) | ||||
|   storageState.origins[0].localStorage[1].value = `const distance = sqrt(20) | ||||
|   const part001 = startSketchOn('-XZ') | ||||
|     |> startProfileAt([-6.95, 4.98], %) | ||||
|     |> line([25.1, 0.41], %) | ||||
|     |> line([0.73, -14.93], %) | ||||
|     |> line([-23.44, 0.52], %) | ||||
|     |> close(%) | ||||
|   ` | ||||
|   test.use({ storageState }) | ||||
|   const u = getUtils(page) | ||||
|   await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|   await page.goto('/') | ||||
|   await u.waitForAuthSkipAppStart() | ||||
|   await u.openDebugPanel() | ||||
|   await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|  | ||||
|   test('Can extrude from the command bar', async ({ page, context }) => { | ||||
|     const u = getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.goto('/') | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|   let cmdSearchBar = page.getByPlaceholder('Search commands') | ||||
|   await page.keyboard.press('Meta+K') | ||||
|   await expect(cmdSearchBar).toBeVisible() | ||||
|  | ||||
|     // Make sure the stream is up | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.closeDebugPanel() | ||||
|   // Search for extrude command and choose it | ||||
|   await page.getByRole('option', { name: 'Extrude' }).click() | ||||
|   await expect(page.locator('#arg-form > label')).toContainText( | ||||
|     'Please select one face' | ||||
|   ) | ||||
|   await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled() | ||||
|  | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Start Sketch' }) | ||||
|     ).not.toBeDisabled() | ||||
|     await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click() | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Extrude' }) | ||||
|     ).not.toBeDisabled() | ||||
|   // Click to select face and set distance | ||||
|   await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click() | ||||
|   await page.getByRole('button', { name: 'Continue' }).click() | ||||
|  | ||||
|     let cmdSearchBar = page.getByPlaceholder('Search commands') | ||||
|     await page.keyboard.press('Meta+K') | ||||
|     await expect(cmdSearchBar).toBeVisible() | ||||
|   // Assert that we're on the distance step | ||||
|   await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled() | ||||
|  | ||||
|     // Search for extrude command and choose it | ||||
|     await page.getByRole('option', { name: 'Extrude' }).click() | ||||
|   // Assert that the an alternative variable name is chosen, | ||||
|   // since the default variable name is already in use (distance) | ||||
|   await page.getByRole('button', { name: 'Create new variable' }).click() | ||||
|   await expect(page.getByPlaceholder('Variable name')).toHaveValue( | ||||
|     'distance001' | ||||
|   ) | ||||
|   await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled() | ||||
|   await page.getByRole('button', { name: 'Continue' }).click() | ||||
|  | ||||
|     // Assert that we're on the distance step | ||||
|     await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled() | ||||
|   // Review step and argument hotkeys | ||||
|   await expect( | ||||
|     page.getByRole('button', { name: 'Submit command' }) | ||||
|   ).toBeEnabled() | ||||
|   await page.keyboard.press('Backspace') | ||||
|   await expect( | ||||
|     page.getByRole('button', { name: 'Distance 12', exact: false }) | ||||
|   ).toBeDisabled() | ||||
|   await page.keyboard.press('Enter') | ||||
|  | ||||
|     // Assert that the an alternative variable name is chosen, | ||||
|     // since the default variable name is already in use (distance) | ||||
|     await page.getByRole('button', { name: 'Create new variable' }).click() | ||||
|     await expect(page.getByPlaceholder('Variable name')).toHaveValue( | ||||
|       'distance001' | ||||
|     ) | ||||
|     await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled() | ||||
|     await page.getByRole('button', { name: 'Continue' }).click() | ||||
|   await expect(page.getByText('Confirm Extrude')).toBeVisible() | ||||
|  | ||||
|     // Review step and argument hotkeys | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Submit command' }) | ||||
|     ).toBeEnabled() | ||||
|     await page.keyboard.press('Backspace') | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Distance 12', exact: false }) | ||||
|     ).toBeDisabled() | ||||
|     await page.keyboard.press('Enter') | ||||
|  | ||||
|     await expect(page.getByText('Confirm Extrude')).toBeVisible() | ||||
|  | ||||
|     // Check that the code was updated | ||||
|     await page.keyboard.press('Enter') | ||||
|     // Unfortunately this indentation seems to matter for the test | ||||
|     await expect(page.locator('.cm-content')).toHaveText( | ||||
|       `const distance = sqrt(20) | ||||
|   // Check that the code was updated | ||||
|   await page.keyboard.press('Enter') | ||||
|   // Unfortunately this indentation seems to matter for the test | ||||
|   await expect(page.locator('.cm-content')).toHaveText( | ||||
|     `const distance = sqrt(20) | ||||
| const distance001 = 5 + 7 | ||||
| const part001 = startSketchOn('-XZ') | ||||
|     |> startProfileAt([-6.95, 4.98], %) | ||||
|     |> line([25.1, 0.41], %) | ||||
|     |> line([0.73, -14.93], %) | ||||
|     |> line([-23.44, 0.52], %) | ||||
|     |> close(%) | ||||
|     |> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines | ||||
|     ) | ||||
|   }) | ||||
|   |> startProfileAt([-6.95, 4.98], %) | ||||
|   |> line([25.1, 0.41], %) | ||||
|   |> line([0.73, -14.93], %) | ||||
|   |> line([-23.44, 0.52], %) | ||||
|   |> close(%) | ||||
|   |> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines | ||||
|   ) | ||||
| }) | ||||
|  | ||||
| test('Can add multiple sketches', async ({ page }) => { | ||||
| @ -960,13 +918,13 @@ test('Can add multiple sketches', async ({ page }) => { | ||||
|   await page.waitForTimeout(500) // TODO detect animation ending, or disable animation | ||||
|  | ||||
|   const startXPx = 600 | ||||
|   await u.closeDebugPanel() | ||||
|   await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toHaveText(`const part001 = startSketchOn('-XZ') | ||||
|   |> startProfileAt(${commonPoints.startAt}, %)`) | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   await u.closeDebugPanel() | ||||
|   await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
| @ -1414,138 +1372,10 @@ test('Snap to close works (at any scale)', async ({ page }) => { | ||||
|   ) => `const part001 = startSketchOn('XZ') | ||||
| |> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %) | ||||
| |> line([${roundOff(scale * 175.36)}, 0], %) | ||||
| |> line([0, -${roundOff(scale * 175.36) + fudge}], %) | ||||
| |> line([0, -${roundOff(scale * 175.37) + fudge}], %) | ||||
| |> close(%)` | ||||
|  | ||||
|   await doSnapAtDifferentScales([0, 100, 100], codeTemplate(0.01, 0.01)) | ||||
|  | ||||
|   await doSnapAtDifferentScales([0, 10000, 10000], codeTemplate()) | ||||
| }) | ||||
|  | ||||
| test('Sketch on face', async ({ page, context }) => { | ||||
|   const u = getUtils(page) | ||||
|   await context.addInitScript(async () => { | ||||
|     localStorage.setItem( | ||||
|       'persistCode', | ||||
|       `const part001 = startSketchOn('-XZ') | ||||
|   |> startProfileAt([3.29, 7.86], %) | ||||
|   |> line([2.48, 2.44], %) | ||||
|   |> line([2.66, 1.17], %) | ||||
|   |> line([3.75, 0.46], %) | ||||
|   |> line([4.99, -0.46], %) | ||||
|   |> line([3.3, -2.12], %) | ||||
|   |> line([2.16, -3.33], %) | ||||
|   |> line([0.85, -3.08], %) | ||||
|   |> line([-0.18, -3.36], %) | ||||
|   |> line([-3.86, -2.73], %) | ||||
|   |> line([-17.67, 0.85], %) | ||||
|   |> close(%) | ||||
|   |> extrude(5 + 7, %)` | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|   await page.goto('/') | ||||
|   await u.waitForAuthSkipAppStart() | ||||
|   await expect( | ||||
|     page.getByRole('button', { name: 'Start Sketch' }) | ||||
|   ).not.toBeDisabled() | ||||
|  | ||||
|   await page.getByRole('button', { name: 'Start Sketch' }).click() | ||||
|  | ||||
|   let previousCodeContent = await page.locator('.cm-content').innerText() | ||||
|  | ||||
|   await page.mouse.click(793, 133) | ||||
|  | ||||
|   const firstClickPosition = [612, 238] | ||||
|   const secondClickPosition = [661, 242] | ||||
|   const thirdClickPosition = [609, 267] | ||||
|  | ||||
|   await page.waitForTimeout(300) | ||||
|  | ||||
|   await page.mouse.click(firstClickPosition[0], firstClickPosition[1]) | ||||
|   await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) | ||||
|   previousCodeContent = await page.locator('.cm-content').innerText() | ||||
|  | ||||
|   await page.mouse.click(secondClickPosition[0], secondClickPosition[1]) | ||||
|   await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) | ||||
|   previousCodeContent = await page.locator('.cm-content').innerText() | ||||
|  | ||||
|   await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1]) | ||||
|   await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) | ||||
|   previousCodeContent = await page.locator('.cm-content').innerText() | ||||
|  | ||||
|   await page.mouse.click(firstClickPosition[0], firstClickPosition[1]) | ||||
|   await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) | ||||
|   previousCodeContent = await page.locator('.cm-content').innerText() | ||||
|  | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toContainText(`const part002 = startSketchOn(part001, 'seg01') | ||||
|   |> startProfileAt([1.03, 1.03], %) | ||||
|   |> line([4.18, -0.35], %) | ||||
|   |> line([-4.44, -2.13], %) | ||||
|   |> close(%)`) | ||||
|  | ||||
|   await u.openAndClearDebugPanel() | ||||
|   await page.getByRole('button', { name: 'Exit Sketch' }).click() | ||||
|   await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|  | ||||
|   await u.updateCamPosition([1049, 239, 686]) | ||||
|   await u.closeDebugPanel() | ||||
|  | ||||
|   await page.getByText('startProfileAt([1.03, 1.03], %)').click() | ||||
|   await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() | ||||
|   await page.getByRole('button', { name: 'Edit Sketch' }).click() | ||||
|   await page.setViewportSize({ width: 1200, height: 1200 }) | ||||
|   await u.openAndClearDebugPanel() | ||||
|   await u.updateCamPosition([452, -152, 1166]) | ||||
|   await u.closeDebugPanel() | ||||
|   await page.waitForTimeout(200) | ||||
|  | ||||
|   const pointToDragFirst = [787, 565] | ||||
|   await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1]) | ||||
|   await page.mouse.down() | ||||
|   await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], { | ||||
|     steps: 5, | ||||
|   }) | ||||
|   await page.mouse.up() | ||||
|   await page.waitForTimeout(100) | ||||
|   await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) | ||||
|   previousCodeContent = await page.locator('.cm-content').innerText() | ||||
|  | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toContainText(`const part002 = startSketchOn(part001, 'seg01') | ||||
| |> startProfileAt([1.03, 1.03], %) | ||||
| |> line([${process?.env?.CI ? 2.74 : 2.93}, -${ | ||||
|     process?.env?.CI ? 0.24 : 0.2 | ||||
|   }], %) | ||||
| |> line([-4.44, -2.13], %) | ||||
| |> close(%)`) | ||||
|  | ||||
|   // exit sketch | ||||
|   await u.openAndClearDebugPanel() | ||||
|   await page.getByRole('button', { name: 'Exit Sketch' }).click() | ||||
|   await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|  | ||||
|   await page.getByText('startProfileAt([1.03, 1.03], %)').click() | ||||
|  | ||||
|   await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled() | ||||
|   await page.getByRole('button', { name: 'Extrude' }).click() | ||||
|  | ||||
|   await expect(page.getByTestId('command-bar')).toBeVisible() | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   await page.keyboard.press('Enter') | ||||
|   await expect(page.getByText('Confirm Extrude')).toBeVisible() | ||||
|   await page.keyboard.press('Enter') | ||||
|  | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toContainText(`const part002 = startSketchOn(part001, 'seg01') | ||||
| |> startProfileAt([1.03, 1.03], %) | ||||
| |> line([${process?.env?.CI ? 2.74 : 2.93}, -${ | ||||
|     process?.env?.CI ? 0.24 : 0.2 | ||||
|   }], %) | ||||
| |> line([-4.44, -2.13], %) | ||||
| |> close(%) | ||||
| |> extrude(5 + 7, %)`) | ||||
| }) | ||||
|  | ||||
| @ -7,18 +7,30 @@ import { spawn } from 'child_process' | ||||
| import { APP_NAME } from 'lib/constants' | ||||
| import JSZip from 'jszip' | ||||
| import path from 'path' | ||||
| import { basicSettings, basicStorageState } from './storageStates' | ||||
| import * as TOML from '@iarna/toml' | ||||
|  | ||||
| test.beforeEach(async ({ page }) => { | ||||
| test.beforeEach(async ({ context, page }) => { | ||||
|   await context.addInitScript(async (token) => { | ||||
|     localStorage.setItem('TOKEN_PERSIST_KEY', token) | ||||
|     localStorage.setItem('persistCode', ``) | ||||
|     localStorage.setItem( | ||||
|       'SETTINGS_PERSIST_KEY', | ||||
|       JSON.stringify({ | ||||
|         baseUnit: 'in', | ||||
|         cameraControls: 'KittyCAD', | ||||
|         defaultDirectory: '', | ||||
|         defaultProjectName: 'project-$nnn', | ||||
|         onboardingStatus: 'dismissed', | ||||
|         showDebugPanel: true, | ||||
|         textWrapping: 'On', | ||||
|         theme: 'system', | ||||
|         unitSystem: 'imperial', | ||||
|       }) | ||||
|     ) | ||||
|   }, secrets.token) | ||||
|   // reducedMotion kills animations, which speeds up tests and reduces flakiness | ||||
|   await page.emulateMedia({ reducedMotion: 'reduce' }) | ||||
| }) | ||||
|  | ||||
| test.use({ | ||||
|   storageState: structuredClone(basicStorageState), | ||||
| }) | ||||
|  | ||||
| test.setTimeout(60_000) | ||||
|  | ||||
| test('exports of each format should work', async ({ page, context }) => { | ||||
| @ -320,22 +332,6 @@ test('extrude on each default plane should be stable', async ({ | ||||
|   page, | ||||
|   context, | ||||
| }) => { | ||||
|   await context.addInitScript(async () => { | ||||
|     localStorage.setItem( | ||||
|       'SETTINGS_PERSIST_KEY', | ||||
|       JSON.stringify({ | ||||
|         baseUnit: 'in', | ||||
|         cameraControls: 'KittyCAD', | ||||
|         defaultDirectory: '', | ||||
|         defaultProjectName: 'project-$nnn', | ||||
|         onboardingStatus: 'dismissed', | ||||
|         showDebugPanel: true, | ||||
|         textWrapping: 'On', | ||||
|         theme: 'dark', | ||||
|         unitSystem: 'imperial', | ||||
|       }) | ||||
|     ) | ||||
|   }) | ||||
|   const u = getUtils(page) | ||||
|   const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}') | ||||
|   |> startProfileAt([7.00, 4.40], %) | ||||
| @ -357,26 +353,29 @@ test('extrude on each default plane should be stable', async ({ | ||||
|   await u.openDebugPanel() | ||||
|   await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|   await u.clearAndCloseDebugPanel() | ||||
|   await page.waitForTimeout(200) | ||||
|  | ||||
|   await page.getByText('Code').click() | ||||
|   await expect(page).toHaveScreenshot({ | ||||
|     maxDiffPixels: 100, | ||||
|   }) | ||||
|   await page.getByText('Code').click() | ||||
|  | ||||
|   const runSnapshotsForOtherPlanes = async (plane = 'XY') => { | ||||
|     // clear code | ||||
|     await u.removeCurrentCode() | ||||
|     // add makeCode('XZ') | ||||
|     await u.openAndClearDebugPanel() | ||||
|     await page.locator('.cm-content').fill(makeCode(plane)) | ||||
|     // wait for execution done | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.clearAndCloseDebugPanel() | ||||
|  | ||||
|     await page.getByText('Code').click() | ||||
|     await page.waitForTimeout(150) | ||||
|     await expect(page).toHaveScreenshot({ | ||||
|       maxDiffPixels: 100, | ||||
|     }) | ||||
|     await page.getByText('Code').click() | ||||
|   } | ||||
|   await runSnapshotsForOtherPlanes('XY') | ||||
|   await runSnapshotsForOtherPlanes('-XY') | ||||
|  | ||||
|   await runSnapshotsForOtherPlanes('XZ') | ||||
| @ -387,6 +386,22 @@ test('extrude on each default plane should be stable', async ({ | ||||
| }) | ||||
|  | ||||
| test('Draft segments should look right', async ({ page, context }) => { | ||||
|   await context.addInitScript(async () => { | ||||
|     localStorage.setItem( | ||||
|       'SETTINGS_PERSIST_KEY', | ||||
|       JSON.stringify({ | ||||
|         baseUnit: 'in', | ||||
|         cameraControls: 'KittyCAD', | ||||
|         defaultDirectory: '', | ||||
|         defaultProjectName: 'project-$nnn', | ||||
|         onboardingStatus: 'dismissed', | ||||
|         showDebugPanel: true, | ||||
|         textWrapping: 'On', | ||||
|         theme: 'system', | ||||
|         unitSystem: 'imperial', | ||||
|       }) | ||||
|     ) | ||||
|   }) | ||||
|   const u = getUtils(page) | ||||
|   await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|   const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
| @ -445,9 +460,26 @@ test('Draft segments should look right', async ({ page, context }) => { | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test('Client side scene scale should match engine scale - Inch', async ({ | ||||
| test('Client side scene scale should match engine scale inch', async ({ | ||||
|   page, | ||||
|   context, | ||||
| }) => { | ||||
|   await context.addInitScript(async () => { | ||||
|     localStorage.setItem( | ||||
|       'SETTINGS_PERSIST_KEY', | ||||
|       JSON.stringify({ | ||||
|         baseUnit: 'in', | ||||
|         cameraControls: 'KittyCAD', | ||||
|         defaultDirectory: '', | ||||
|         defaultProjectName: 'project-$nnn', | ||||
|         onboardingStatus: 'dismissed', | ||||
|         showDebugPanel: true, | ||||
|         textWrapping: 'On', | ||||
|         theme: 'system', | ||||
|         unitSystem: 'imperial', | ||||
|       }) | ||||
|     ) | ||||
|   }) | ||||
|   const u = getUtils(page) | ||||
|   await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|   const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
| @ -480,7 +512,7 @@ test('Client side scene scale should match engine scale - Inch', async ({ | ||||
|   await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toHaveText(`const part001 = startSketchOn('-XZ') | ||||
|   |> startProfileAt([9.06, -12.22], %)`) | ||||
| |> startProfileAt([9.06, -12.22], %)`) | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   await u.closeDebugPanel() | ||||
| @ -490,8 +522,8 @@ test('Client side scene scale should match engine scale - Inch', async ({ | ||||
|  | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toHaveText(`const part001 = startSketchOn('-XZ') | ||||
|   |> startProfileAt([9.06, -12.22], %) | ||||
|   |> line([9.14, 0], %)`) | ||||
| |> startProfileAt([9.06, -12.22], %) | ||||
| |> line([9.14, 0], %)`) | ||||
|  | ||||
|   await page.getByRole('button', { name: 'Tangential Arc' }).click() | ||||
|   await page.waitForTimeout(100) | ||||
| @ -500,9 +532,9 @@ test('Client side scene scale should match engine scale - Inch', async ({ | ||||
|  | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toHaveText(`const part001 = startSketchOn('-XZ') | ||||
|   |> startProfileAt([9.06, -12.22], %) | ||||
|   |> line([9.14, 0], %) | ||||
|   |> tangentialArcTo([27.34, -3.08], %)`) | ||||
| |> startProfileAt([9.06, -12.22], %) | ||||
| |> line([9.14, 0], %) | ||||
| |> tangentialArcTo([27.34, -3.08], %)`) | ||||
|  | ||||
|   // click tangential arc tool again to unequip it | ||||
|   await page.getByRole('button', { name: 'Tangential Arc' }).click() | ||||
| @ -528,142 +560,100 @@ test('Client side scene scale should match engine scale - Inch', async ({ | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test.describe('Client side scene scale should match engine scale - Millimeters', () => { | ||||
|   const storageState = structuredClone(basicStorageState) | ||||
|   storageState.origins[0].localStorage[2].value = TOML.stringify({ | ||||
|     settings: { | ||||
|       ...basicSettings, | ||||
|       modeling: { | ||||
|         ...basicSettings.modeling, | ||||
|         defaultUnit: 'mm', | ||||
|       }, | ||||
|     }, | ||||
|   }) | ||||
|   test.use({ | ||||
|     storageState, | ||||
|   }) | ||||
|  | ||||
|   test('Millimeters', async ({ page }) => { | ||||
|     const u = getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.goto('/') | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await u.openDebugPanel() | ||||
|  | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Start Sketch' }) | ||||
|     ).not.toBeDisabled() | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Start Sketch' }) | ||||
|     ).toBeVisible() | ||||
|  | ||||
|     // click on "Start Sketch" button | ||||
|     await u.clearCommandLogs() | ||||
|     await u.doAndWaitForImageDiff( | ||||
|       () => page.getByRole('button', { name: 'Start Sketch' }).click(), | ||||
|       200 | ||||
|     ) | ||||
|  | ||||
|     // select a plane | ||||
|     await page.mouse.click(700, 200) | ||||
|  | ||||
|     await expect(page.locator('.cm-content')).toHaveText( | ||||
|       `const part001 = startSketchOn('-XZ')` | ||||
|     ) | ||||
|  | ||||
|     await page.waitForTimeout(300) // TODO detect animation ending, or disable animation | ||||
|  | ||||
|     const startXPx = 600 | ||||
|     await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`const part001 = startSketchOn('-XZ') | ||||
|       |> startProfileAt([230.03, -310.32], %)`) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     await u.closeDebugPanel() | ||||
|  | ||||
|     await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`const part001 = startSketchOn('-XZ') | ||||
|       |> startProfileAt([230.03, -310.32], %) | ||||
|       |> line([232.2, 0], %)`) | ||||
|  | ||||
|     await page.getByRole('button', { name: 'Tangential Arc' }).click() | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`const part001 = startSketchOn('-XZ') | ||||
|       |> startProfileAt([230.03, -310.32], %) | ||||
|       |> line([232.2, 0], %) | ||||
|       |> tangentialArcTo([694.43, -78.12], %)`) | ||||
|  | ||||
|     await page.getByRole('button', { name: 'Tangential Arc' }).click() | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     // screen shot should show the sketch | ||||
|     await expect(page).toHaveScreenshot({ | ||||
|       maxDiffPixels: 100, | ||||
|     }) | ||||
|  | ||||
|     // exit sketch | ||||
|     await u.openAndClearDebugPanel() | ||||
|     await page.getByRole('button', { name: 'Exit Sketch' }).click() | ||||
|  | ||||
|     // wait for execution done | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.clearAndCloseDebugPanel() | ||||
|     await page.waitForTimeout(200) | ||||
|  | ||||
|     // second screen shot should look almost identical, i.e. scale should be the same. | ||||
|     await expect(page).toHaveScreenshot({ | ||||
|       maxDiffPixels: 100, | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test('Sketch on face with none z-up', async ({ page, context }) => { | ||||
|   const u = getUtils(page) | ||||
| test('Client side scene scale should match engine scale mm', async ({ | ||||
|   page, | ||||
|   context, | ||||
| }) => { | ||||
|   await context.addInitScript(async () => { | ||||
|     localStorage.setItem( | ||||
|       'persistCode', | ||||
|       `const part001 = startSketchOn('-XZ') | ||||
|   |> startProfileAt([1.4, 2.47], %) | ||||
|   |> line([9.31, 10.55], %, 'seg01') | ||||
|   |> line([11.91, -10.42], %) | ||||
|   |> close(%) | ||||
|   |> extrude(5 + 7, %) | ||||
| const part002 = startSketchOn(part001, 'seg01') | ||||
|   |> startProfileAt([-2.89, 1.82], %) | ||||
|   |> line([4.68, 3.05], %) | ||||
|   |> line([0, -7.79], %, 'seg02') | ||||
|   |> close(%) | ||||
|   |> extrude(5 + 7, %) | ||||
| ` | ||||
|       'SETTINGS_PERSIST_KEY', | ||||
|       JSON.stringify({ | ||||
|         baseUnit: 'mm', | ||||
|         cameraControls: 'KittyCAD', | ||||
|         defaultDirectory: '', | ||||
|         defaultProjectName: 'project-$nnn', | ||||
|         onboardingStatus: 'dismissed', | ||||
|         showDebugPanel: true, | ||||
|         textWrapping: 'On', | ||||
|         theme: 'system', | ||||
|         unitSystem: 'metric', | ||||
|       }) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   const u = getUtils(page) | ||||
|   await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|   const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|   await page.goto('/') | ||||
|   await u.waitForAuthSkipAppStart() | ||||
|   await u.openDebugPanel() | ||||
|  | ||||
|   await expect( | ||||
|     page.getByRole('button', { name: 'Start Sketch' }) | ||||
|   ).not.toBeDisabled() | ||||
|   await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible() | ||||
|  | ||||
|   await page.getByRole('button', { name: 'Start Sketch' }).click() | ||||
|   let previousCodeContent = await page.locator('.cm-content').innerText() | ||||
|   // click on "Start Sketch" button | ||||
|   await u.clearCommandLogs() | ||||
|   await u.doAndWaitForImageDiff( | ||||
|     () => page.getByRole('button', { name: 'Start Sketch' }).click(), | ||||
|     200 | ||||
|   ) | ||||
|  | ||||
|   // click at 641, 135 | ||||
|   await page.mouse.click(641, 135) | ||||
|   await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) | ||||
|   previousCodeContent = await page.locator('.cm-content').innerText() | ||||
|   // select a plane | ||||
|   await page.mouse.click(700, 200) | ||||
|  | ||||
|   await page.waitForTimeout(300) | ||||
|   await expect(page.locator('.cm-content')).toHaveText( | ||||
|     `const part001 = startSketchOn('-XZ')` | ||||
|   ) | ||||
|  | ||||
|   await page.waitForTimeout(300) // TODO detect animation ending, or disable animation | ||||
|  | ||||
|   const startXPx = 600 | ||||
|   await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toHaveText(`const part001 = startSketchOn('-XZ') | ||||
|   |> startProfileAt([230.03, -310.33], %)`) | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   await u.closeDebugPanel() | ||||
|  | ||||
|   await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toHaveText(`const part001 = startSketchOn('-XZ') | ||||
|   |> startProfileAt([230.03, -310.33], %) | ||||
|   |> line([232.2, 0], %)`) | ||||
|  | ||||
|   await page.getByRole('button', { name: 'Tangential Arc' }).click() | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) | ||||
|  | ||||
|   await expect(page.locator('.cm-content')) | ||||
|     .toHaveText(`const part001 = startSketchOn('-XZ') | ||||
|   |> startProfileAt([230.03, -310.33], %) | ||||
|   |> line([232.2, 0], %) | ||||
|   |> tangentialArcTo([694.43, -78.12], %)`) | ||||
|  | ||||
|   await page.getByRole('button', { name: 'Tangential Arc' }).click() | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   // screen shot should show the sketch | ||||
|   await expect(page).toHaveScreenshot({ | ||||
|     maxDiffPixels: 100, | ||||
|   }) | ||||
|  | ||||
|   // exit sketch | ||||
|   await u.openAndClearDebugPanel() | ||||
|   await page.getByRole('button', { name: 'Exit Sketch' }).click() | ||||
|  | ||||
|   // wait for execution done | ||||
|   await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|   await u.clearAndCloseDebugPanel() | ||||
|   await page.waitForTimeout(200) | ||||
|  | ||||
|   // second screen shot should look almost identical, i.e. scale should be the same. | ||||
|   await expect(page).toHaveScreenshot({ | ||||
|     maxDiffPixels: 100, | ||||
|   }) | ||||
|  | ||||
| Before Width: | Height: | Size: 43 KiB | 
| Before Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 45 KiB | 
| Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB | 
| Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB | 
| Before Width: | Height: | Size: 79 KiB | 
