Compare commits

..

2 Commits

Author SHA1 Message Date
ddce447c0b A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-03-20 18:37:48 +00:00
e8769eb543 Add ability to write prelude in KCL 2024-03-20 14:31:42 -04:00
471 changed files with 7693 additions and 24142 deletions

View File

@ -1,3 +1,3 @@
[codespell] [codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas skip: **/target,node_modules,build,**/Cargo.lock

View File

@ -7,23 +7,23 @@ on:
- '**/Cargo.toml' - '**/Cargo.toml'
- '**/Cargo.lock' - '**/Cargo.lock'
- '**/rust-toolchain.toml' - '**/rust-toolchain.toml'
- .github/workflows/cargo-bench.yml - .github/workflows/cargo-criterion.yml
pull_request: pull_request:
paths: paths:
- '**.rs' - '**.rs'
- '**/Cargo.toml' - '**/Cargo.toml'
- '**/Cargo.lock' - '**/Cargo.lock'
- '**/rust-toolchain.toml' - '**/rust-toolchain.toml'
- .github/workflows/cargo-bench.yml - .github/workflows/cargo-criterion.yml
workflow_dispatch: workflow_dispatch:
permissions: read-all permissions: read-all
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
name: cargo bench name: cargo criterion
jobs: jobs:
cargo-bench: cargocriterion:
name: Benchmark with iai name: cargo criterion
runs-on: ubuntu-latest-8-cores runs-on: ubuntu-latest-8-cores
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -31,12 +31,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
cargo install cargo-criterion cargo install cargo-criterion
sudo apt update
sudo apt install -y valgrind
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1 uses: Swatinem/rust-cache@v2.6.1
- name: Benchmark kcl library - name: Benchmark kcl library
shell: bash shell: bash
run: |- run: |-
cd src/wasm-lib/kcl; cargo bench -- iai cd src/wasm-lib/kcl; cargo criterion

View File

@ -16,10 +16,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Comment on PR - name: Comment on PR
uses: actions/github-script@v7 uses: actions/github-script@v6
with: with:
script: | script: |
const message = '`src/lib/exampleKcl.ts` has been updated in this PR, please review and update the `src/routes/onboarding`, if needed.'; const message = '`src/lib/exampleKcl.ts` has been updated in this PR, please review and update the `src/routes/onboarding`, if needed.';

View File

@ -125,9 +125,6 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-14, ubuntu-latest, windows-latest] os: [macos-14, ubuntu-latest, windows-latest]
env:
TAURI_ARGS_MACOS: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
TAURI_ARGS_UBUNTU: ${{ matrix.os == 'ubuntu-latest' && '--bundles' || '' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -147,12 +144,10 @@ jobs:
sudo apt-get update && sudo apt-get update &&
sudo apt-get install -y sudo apt-get install -y
libgtk-3-dev libgtk-3-dev
libayatana-appindicator3-dev libgtksourceview-3.0-dev
webkit2gtk-4.0
libappindicator3-dev
webkit2gtk-driver webkit2gtk-driver
libsoup-3.0-dev
libjavascriptcoregtk-4.1-dev
libwebkit2gtk-4.1-dev
at-spi2-core
xvfb xvfb
- name: Sync node version and setup cache - name: Sync node version and setup cache
@ -166,9 +161,7 @@ jobs:
- name: Setup Rust - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
# TODO: re-enable for Windows builds, see https://github.com/tauri-apps/tauri/issues/9045
- name: Setup Rust cache - name: Setup Rust cache
if: matrix.os != 'windows-latest'
uses: swatinem/rust-cache@v2 uses: swatinem/rust-cache@v2
with: with:
workspaces: './src-tauri -> target' workspaces: './src-tauri -> target'
@ -231,14 +224,14 @@ jobs:
with: with:
includeRelease: false includeRelease: false
includeDebug: true includeDebug: true
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}" args: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
- name: Build the app (release) and sign - name: Build the app (release) and sign
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
if: ${{ env.BUILD_RELEASE == 'true' }} if: ${{ env.BUILD_RELEASE == 'true' }}
env: env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
@ -247,7 +240,7 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}" TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
with: with:
args: "${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}" args: "${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest' if: matrix.os != 'ubuntu-latest'
@ -257,11 +250,10 @@ jobs:
with: with:
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*" path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
# TODO: re-enable linux e2e tests when possible
- name: Run e2e tests (linux only) - name: Run e2e tests (linux only)
if: false if: matrix.os == 'ubuntu-latest'
run: | run: |
cargo install tauri-driver cargo install tauri-driver@0.1.3
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }} source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
export VITE_KC_API_BASE_URL export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri xvfb-run yarn test:e2e:tauri

View File

@ -9,10 +9,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions:
contents: write
pull-requests: write
jobs: jobs:
playwright-ubuntu: playwright-ubuntu:
timeout-minutes: 60 timeout-minutes: 60

1
.gitignore vendored
View File

@ -51,6 +51,5 @@ e2e/playwright/export-snapshots/*
## generated files ## generated files
src/**/*.typegen.ts src/**/*.typegen.ts
src-tauri/gen
src/wasm-lib/grackle/stdlib_cube_partial.json src/wasm-lib/grackle/stdlib_cube_partial.json

View File

@ -281,7 +281,7 @@ https://github.com/KittyCAD/modeling-app/assets/29681384/6f5e8e85-1003-4fd9-be7f
<details> <details>
<summary> <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> </summary>
```JSON ```JSON

View File

@ -12,10 +12,6 @@ Computes the absolute value of a number.
abs(num: number) -> number abs(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Computes the arccosine of a number (in radians).
acos(num: number) -> number acos(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,10 +12,6 @@ Computes the arcsine of a number (in radians).
asin(num: number) -> number asin(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Computes the arctangent of a number (in radians).
atan(num: number) -> number atan(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

File diff suppressed because one or more lines are too long

View File

@ -12,10 +12,6 @@ Computes the smallest integer greater than or equal to a number.
ceil(num: number) -> number ceil(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,10 @@
--- ---
title: "cos" title: "cos"
excerpt: "Computes the cosine of a number (in radians)." excerpt: "Computes the sine of a number (in radians)."
layout: manual 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 cos(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Return the value of Eulers number `e`.
e() -> number e() -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,10 +12,6 @@ Computes the largest integer less than or equal to a number.
floor(num: number) -> number floor(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -30,12 +30,10 @@ layout: manual
* [`extrude`](kcl/extrude) * [`extrude`](kcl/extrude)
* [`fillet`](kcl/fillet) * [`fillet`](kcl/fillet)
* [`floor`](kcl/floor) * [`floor`](kcl/floor)
* [`getEdge`](kcl/getEdge)
* [`getExtrudeWallTransform`](kcl/getExtrudeWallTransform) * [`getExtrudeWallTransform`](kcl/getExtrudeWallTransform)
* [`getNextAdjacentEdge`](kcl/getNextAdjacentEdge) * [`getNextAdjacentEdge`](kcl/getNextAdjacentEdge)
* [`getOppositeEdge`](kcl/getOppositeEdge) * [`getOppositeEdge`](kcl/getOppositeEdge)
* [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge)
* [`helix`](kcl/helix)
* [`hole`](kcl/hole) * [`hole`](kcl/hole)
* [`import`](kcl/import) * [`import`](kcl/import)
* [`lastSegX`](kcl/lastSegX) * [`lastSegX`](kcl/lastSegX)
@ -57,7 +55,6 @@ layout: manual
* [`patternLinear3d`](kcl/patternLinear3d) * [`patternLinear3d`](kcl/patternLinear3d)
* [`pi`](kcl/pi) * [`pi`](kcl/pi)
* [`pow`](kcl/pow) * [`pow`](kcl/pow)
* [`revolve`](kcl/revolve)
* [`segAng`](kcl/segAng) * [`segAng`](kcl/segAng)
* [`segEndX`](kcl/segEndX) * [`segEndX`](kcl/segEndX)
* [`segEndY`](kcl/segEndY) * [`segEndY`](kcl/segEndY)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,10 +12,6 @@ Returns the angle of the given leg for x.
legAngX(hypotenuse: number, leg: number) -> number legAngX(hypotenuse: number, leg: number) -> number
``` ```
### Tags
* `utilities`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Returns the angle of the given leg for y.
legAngY(hypotenuse: number, leg: number) -> number legAngY(hypotenuse: number, leg: number) -> number
``` ```
### Tags
* `utilities`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Returns the length of the given leg.
legLen(hypotenuse: number, leg: number) -> number legLen(hypotenuse: number, leg: number) -> number
``` ```
### Tags
* `utilities`
### Examples ### Examples
```js ```js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,10 +12,6 @@ Computes the natural logarithm of the number.
ln(num: number) -> number ln(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ The result might not be correctly rounded owing to implementation details; `log2
log(num: number, base: number) -> number log(num: number, base: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Computes the base 10 logarithm of the number.
log10(num: number) -> number log10(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Computes the base 2 logarithm of the number.
log2(num: number) -> number log2(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Computes the maximum of the given arguments.
max(args: [number]) -> number max(args: [number]) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Computes the minimum of the given arguments.
min(args: [number]) -> number min(args: [number]) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,10 +12,6 @@ Return the value of `pi`. Archimedes constant (π).
pi() -> number pi() -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Computes the number to a power.
pow(num: number, pow: number) -> number pow(num: number, pow: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,10 +12,6 @@ Computes the sine of a number (in radians).
sin(num: number) -> number sin(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Computes the square root of a number.
sqrt(num: number) -> number sqrt(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -12,10 +12,6 @@ Computes the tangent of a number (in radians).
tan(num: number) -> number tan(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,10 +12,6 @@ Return the value of `tau`. The full circle constant (τ). Equal to 2π.
tau() -> number tau() -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Converts a number from radians to degrees.
toDegrees(num: number) -> number toDegrees(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

View File

@ -12,10 +12,6 @@ Converts a number from degrees to radians.
toRadians(num: number) -> number toRadians(num: number) -> number
``` ```
### Tags
* `math`
### Examples ### Examples
```js ```js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 221 KiB

View File

@ -1,11 +1,10 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { secrets } from './secrets'
import { getUtils } from './test-utils' import { getUtils } from './test-utils'
import waitOn from 'wait-on' import waitOn from 'wait-on'
import { Themes } from '../../src/lib/theme'
import { initialSettings } from '../../src/lib/settings/initialSettings'
import { roundOff } from 'lib/utils' 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 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]', startAt: '[9.06, -12.22]',
num1: 9.14, num1: 9.14,
num2: 18.2, num2: 18.2,
// num1: 9.64,
// num2: 19.19,
} }
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
@ -31,14 +28,31 @@ test.beforeEach(async ({ context, page }) => {
resources: ['tcp:3000'], resources: ['tcp:3000'],
timeout: 5000, 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 // kill animations, speeds up tests and reduced flakiness
await page.emulateMedia({ reducedMotion: 'reduce' }) await page.emulateMedia({ reducedMotion: 'reduce' })
}) })
test.setTimeout(60000) test.setTimeout(60000)
test('Basic sketch', async ({ page, context }) => { test('Basic sketch', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -62,7 +76,6 @@ test('Basic sketch', async ({ page, context }) => {
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')` `const part001 = startSketchOn('-XZ')`
) )
await u.closeDebugPanel()
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
@ -73,6 +86,7 @@ test('Basic sketch', async ({ page, context }) => {
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -458,18 +472,8 @@ test('Auto complete works', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) 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 page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await lspStartPromise
// this test might be brittle as we add and remove functions // this test might be brittle as we add and remove functions
// but should also be easy to update. // but should also be easy to update.
@ -513,133 +517,96 @@ test('Auto complete works', async ({ page }) => {
}) })
// Stored settings validation test // Stored settings validation test
test.describe('Settings persistence and validation tests', () => { test('Stored settings are validated and fall back to defaults', async ({
// Override test setup page,
context,
}) => {
// Override beforeEach test setup
// with corrupted settings // with corrupted settings
const storageState = structuredClone(basicStorageState) await context.addInitScript(async () => {
const s = TOML.parse(storageState.origins[0].localStorage[2].value) as { const storedSettings = JSON.parse(
settings: SaveSettingsPayload localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
} )
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)
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 ({ localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings))
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)
}) })
test('Project settings can be set and override user settings', async ({ await page.setViewportSize({ width: 1200, height: 500 })
page, await page.goto('/', { waitUntil: 'domcontentloaded' })
}) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// Open the settings modal with the browser keyboard shortcut // Check the toast appeared
await page.keyboard.press('Meta+Shift+,') await expect(
page.getByText(`Error validating persisted settings:`, {
exact: false,
})
).toBeVisible()
await expect( // Check the settings were reset
page.getByRole('heading', { name: 'Settings', exact: true }) const storedSettings = JSON.parse(
).toBeVisible() await page.evaluate(
await page () => localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
.locator('select[name="app-theme"]') )
.selectOption({ value: 'light' }) )
await expect(storedSettings.baseUnit).toBe(initialSettings.baseUnit)
// Verify the toast appeared await expect(storedSettings.cameraControls).toBe(
await expect( initialSettings.cameraControls
page.getByText(`Set theme to "light" for this project`) )
).toBeVisible() await expect(storedSettings.defaultDirectory).toBe(
// Check that the theme changed initialSettings.defaultDirectory
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) )
await expect(storedSettings.defaultProjectName).toBe(
// Check that the user setting was not changed initialSettings.defaultProjectName
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')
})
}) })
// Onboarding tests // Onboarding tests
test.describe('Onboarding tests', () => { test('Onboarding redirects and code updating', async ({ page, context }) => {
// Override test setup const u = getUtils(page)
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 }) => { // Override beforeEach test setup
const u = getUtils(page) 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 }) const storedSettings = JSON.parse(
await page.goto('/') localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
await u.waitForAuthSkipAppStart()
// Test that the redirect happened
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
) )
storedSettings.onboardingStatus = '/export'
// Test that you come back to this page when you refresh localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings))
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(/.+/)
}) })
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 }) => { 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 = () => const emptySpaceClick = () =>
page.mouse.click(728, 343).then(() => page.waitForTimeout(100)) page.mouse.click(728, 343).then(() => page.waitForTimeout(100))
const topHorzSegmentClick = () => const topHorzSegmentClick = () =>
page.mouse.click(709, 290).then(() => page.waitForTimeout(100)) page.mouse.click(709, 289).then(() => page.waitForTimeout(100))
const bottomHorzSegmentClick = () => const bottomHorzSegmentClick = () =>
page.mouse.click(767, 396).then(() => page.waitForTimeout(100)) 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 await page.waitForTimeout(700) // wait for animation
const startXPx = 600 const startXPx = 600
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt(${commonPoints.startAt}, %)`)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
@ -759,18 +727,13 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await emptySpaceClick() await emptySpaceClick()
// select segment in editor than another segment in scene and check there are two cursors // 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.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 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 // clear selection by clicking on nothing
await emptySpaceClick() await emptySpaceClick()
@ -800,134 +763,129 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await selectionSequence() await selectionSequence()
}) })
test.describe('Command bar tests', () => { test('Command bar works and can change a setting', async ({ page }) => {
test('Command bar works and can change a setting', async ({ page }) => { // Brief boilerplate
// Brief boilerplate const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' }) 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 // First try opening the command bar and closing it
// It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively // It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively
await page await page
.getByRole('button', { name: 'Ctrl+/' }) .getByRole('button', { name: 'Ctrl+/' })
.or(page.getByRole('button', { name: '⌘K' })) .or(page.getByRole('button', { name: '⌘K' }))
.click() .click()
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await expect(cmdSearchBar).not.toBeVisible() await expect(cmdSearchBar).not.toBeVisible()
// Now try the same, but with the keyboard shortcut, check focus // Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K') await page.keyboard.press('Meta+K')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused() await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar // Try typing in the command bar
await page.keyboard.type('theme') await page.keyboard.type('theme')
const themeOption = page.getByRole('option', { const themeOption = page.getByRole('option', { name: 'Set Theme' })
name: 'Settings · app · theme', await expect(themeOption).toBeVisible()
}) await themeOption.click()
await expect(themeOption).toBeVisible() const themeInput = page.getByPlaceholder('system')
await themeOption.click() await expect(themeInput).toBeVisible()
const themeInput = page.getByPlaceholder('Select an option') await expect(themeInput).toBeFocused()
await expect(themeInput).toBeVisible() // Select dark theme
await expect(themeInput).toBeFocused() await page.keyboard.press('ArrowDown')
// Select dark theme await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowDown') await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
await page.keyboard.press('ArrowDown') 'data-headlessui-state',
await page.keyboard.press('ArrowDown') 'active'
await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute( )
'data-headlessui-state', await page.keyboard.press('Enter')
'active'
// 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 u = getUtils(page)
const storageState = structuredClone(basicStorageState) await page.setViewportSize({ width: 1200, height: 500 })
storageState.origins[0].localStorage[1].value = `const distance = sqrt(20) await page.goto('/')
const part001 = startSketchOn('-XZ') await u.waitForAuthSkipAppStart()
|> startProfileAt([-6.95, 4.98], %) await u.openDebugPanel()
|> line([25.1, 0.41], %) await u.expectCmdLog('[data-message-type="execution-done"]')
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
`
test.use({ storageState })
test('Can extrude from the command bar', async ({ page, context }) => { let cmdSearchBar = page.getByPlaceholder('Search commands')
const u = getUtils(page) await page.keyboard.press('Meta+K')
await page.setViewportSize({ width: 1200, height: 500 }) await expect(cmdSearchBar).toBeVisible()
await page.goto('/')
await u.waitForAuthSkipAppStart()
// Make sure the stream is up // Search for extrude command and choose it
await u.openDebugPanel() await page.getByRole('option', { name: 'Extrude' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]') await expect(page.locator('#arg-form > label')).toContainText(
await u.closeDebugPanel() 'Please select one face'
)
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
await expect( // Click to select face and set distance
page.getByRole('button', { name: 'Start Sketch' }) await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
).not.toBeDisabled() await page.getByRole('button', { name: 'Continue' }).click()
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
await expect(
page.getByRole('button', { name: 'Extrude' })
).not.toBeDisabled()
let cmdSearchBar = page.getByPlaceholder('Search commands') // Assert that we're on the distance step
await page.keyboard.press('Meta+K') await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
await expect(cmdSearchBar).toBeVisible()
// Search for extrude command and choose it // Assert that the an alternative variable name is chosen,
await page.getByRole('option', { name: 'Extrude' }).click() // 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 // Review step and argument hotkeys
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled() 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, await expect(page.getByText('Confirm Extrude')).toBeVisible()
// 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()
// Review step and argument hotkeys // Check that the code was updated
await expect( await page.keyboard.press('Enter')
page.getByRole('button', { name: 'Submit command' }) // Unfortunately this indentation seems to matter for the test
).toBeEnabled() await expect(page.locator('.cm-content')).toHaveText(
await page.keyboard.press('Backspace') `const distance = sqrt(20)
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)
const distance001 = 5 + 7 const distance001 = 5 + 7
const part001 = startSketchOn('-XZ') const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %) |> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %) |> line([25.1, 0.41], %)
|> line([0.73, -14.93], %) |> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %) |> line([-23.44, 0.52], %)
|> close(%) |> close(%)
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines |> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
) )
})
}) })
test('Can add multiple sketches', async ({ page }) => { 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 await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
const startXPx = 600 const startXPx = 600
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -1414,138 +1372,10 @@ test('Snap to close works (at any scale)', async ({ page }) => {
) => `const part001 = startSketchOn('XZ') ) => `const part001 = startSketchOn('XZ')
|> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %) |> startProfileAt([${roundOff(scale * 87.68)}, ${roundOff(scale * 43.84)}], %)
|> line([${roundOff(scale * 175.36)}, 0], %) |> line([${roundOff(scale * 175.36)}, 0], %)
|> line([0, -${roundOff(scale * 175.36) + fudge}], %) |> line([0, -${roundOff(scale * 175.37) + fudge}], %)
|> close(%)` |> close(%)`
await doSnapAtDifferentScales([0, 100, 100], codeTemplate(0.01, 0.01)) await doSnapAtDifferentScales([0, 100, 100], codeTemplate(0.01, 0.01))
await doSnapAtDifferentScales([0, 10000, 10000], codeTemplate()) 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, %)`)
})

View File

@ -7,18 +7,30 @@ import { spawn } from 'child_process'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import JSZip from 'jszip' import JSZip from 'jszip'
import path from 'path' 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 // reducedMotion kills animations, which speeds up tests and reduces flakiness
await page.emulateMedia({ reducedMotion: 'reduce' }) await page.emulateMedia({ reducedMotion: 'reduce' })
}) })
test.use({
storageState: structuredClone(basicStorageState),
})
test.setTimeout(60_000) test.setTimeout(60_000)
test('exports of each format should work', async ({ page, context }) => { 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, page,
context, 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 u = getUtils(page)
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}') const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|> startProfileAt([7.00, 4.40], %) |> startProfileAt([7.00, 4.40], %)
@ -357,26 +353,29 @@ test('extrude on each default plane should be stable', async ({
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel() 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') => { const runSnapshotsForOtherPlanes = async (plane = 'XY') => {
// clear code // clear code
await u.removeCurrentCode() await u.removeCurrentCode()
// add makeCode('XZ') // add makeCode('XZ')
await u.openAndClearDebugPanel()
await page.locator('.cm-content').fill(makeCode(plane)) await page.locator('.cm-content').fill(makeCode(plane))
// wait for execution done // wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
await page.getByText('Code').click() await page.getByText('Code').click()
await page.waitForTimeout(150)
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
await page.getByText('Code').click() await page.getByText('Code').click()
} }
await runSnapshotsForOtherPlanes('XY')
await runSnapshotsForOtherPlanes('-XY') await runSnapshotsForOtherPlanes('-XY')
await runSnapshotsForOtherPlanes('XZ') 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 }) => { 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) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio 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, 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) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio 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 page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %)`) |> startProfileAt([9.06, -12.22], %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
@ -490,8 +522,8 @@ test('Client side scene scale should match engine scale - Inch', async ({
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %) |> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %)`) |> line([9.14, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click() await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.waitForTimeout(100) 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')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([9.06, -12.22], %) |> startProfileAt([9.06, -12.22], %)
|> line([9.14, 0], %) |> line([9.14, 0], %)
|> tangentialArcTo([27.34, -3.08], %)`) |> tangentialArcTo([27.34, -3.08], %)`)
// click tangential arc tool again to unequip it // click tangential arc tool again to unequip it
await page.getByRole('button', { name: 'Tangential Arc' }).click() 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', () => { test('Client side scene scale should match engine scale mm', async ({
const storageState = structuredClone(basicStorageState) page,
storageState.origins[0].localStorage[2].value = TOML.stringify({ context,
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)
await context.addInitScript(async () => { await context.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'SETTINGS_PERSIST_KEY',
`const part001 = startSketchOn('-XZ') JSON.stringify({
|> startProfileAt([1.4, 2.47], %) baseUnit: 'mm',
|> line([9.31, 10.55], %, 'seg01') cameraControls: 'KittyCAD',
|> line([11.91, -10.42], %) defaultDirectory: '',
|> close(%) defaultProjectName: 'project-$nnn',
|> extrude(5 + 7, %) onboardingStatus: 'dismissed',
const part002 = startSketchOn(part001, 'seg01') showDebugPanel: true,
|> startProfileAt([-2.89, 1.82], %) textWrapping: 'On',
|> line([4.68, 3.05], %) theme: 'system',
|> line([0, -7.79], %, 'seg02') unitSystem: 'metric',
|> close(%) })
|> extrude(5 + 7, %)
`
) )
}) })
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Start Sketch' }).click() // click on "Start Sketch" button
let previousCodeContent = await page.locator('.cm-content').innerText() await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
// click at 641, 135 // select a plane
await page.mouse.click(641, 135) await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(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({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Some files were not shown because too many files have changed in this diff Show More