Compare commits

..

1 Commits

Author SHA1 Message Date
078ffa02b0 Add the at token 2024-11-26 16:19:00 -06:00
536 changed files with 141231 additions and 117558 deletions

View File

@ -0,0 +1,59 @@
# bash strict mode
set -euo pipefail
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
if [[ "$3" == ubuntu-latest* ]]; then
yarn test:playwright:browser:chrome:ubuntu -- --shard=$1/$2 || true
elif [[ "$3" == windows-latest* ]]; then
yarn test:playwright:browser:chrome:windows -- --shard=$1/$2 || true
else
echo "Do not run playwright. Unable to detect os runtime."
exit 1
fi
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
retry=1
max_retrys=4
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
if [[ "$3" == ubuntu-latest* ]]; then
yarn test:playwright:browser:chrome:ubuntu -- --last-failed || true
elif [[ "$3" == windows-latest* ]]; then
yarn test:playwright:browser:chrome:windows -- --last-failed || true
else
echo "Do not run playwright. Unable to detect os runtime."
exit 1
fi
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
done
echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
# if it still fails after 3 retrys, then fail the job
exit 1
fi
fi
exit 0

View File

@ -1,17 +1,15 @@
#!/bin/bash
# bash strict mode # bash strict mode
set -euo pipefail set -euo pipefail
if [[ ! -f "test-results/.last-run.json" ]]; then if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally # if no last run artifact, than run plawright normally
echo "run playwright normally" echo "run playwright normally"
if [[ "$3" == ubuntu-latest* ]]; then if [[ "$1" == ubuntu-latest* ]]; then
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --shard=$1/$2 || true xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu || true
elif [[ "$3" == windows-latest* ]]; then elif [[ "$1" == windows-latest* ]]; then
yarn test:playwright:electron:windows -- --shard=$1/$2 || true yarn test:playwright:electron:windows || true
elif [[ "$3" == macos-14* ]]; then elif [[ "$1" == macos-14* ]]; then
yarn test:playwright:electron:macos -- --shard=$1/$2 || true yarn test:playwright:electron:macos || true
else else
echo "Do not run playwright. Unable to detect os runtime." echo "Do not run playwright. Unable to detect os runtime."
exit 1 exit 1
@ -30,11 +28,11 @@ while [[ $retry -le $max_retrys ]]; do
if [[ $failed_tests -gt 0 ]]; then if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry" echo "run playwright with last failed tests and retry $retry"
if [[ "$3" == ubuntu-latest* ]]; then if [[ "$1" == ubuntu-latest* ]]; then
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --last-failed || true xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --last-failed || true
elif [[ "$3" == windows-latest* ]]; then elif [[ "$1" == windows-latest* ]]; then
yarn test:playwright:electron:windows -- --last-failed || true yarn test:playwright:electron:windows -- --last-failed || true
elif [[ "$3" == macos-14* ]]; then elif [[ "$1" == macos-14* ]]; then
yarn test:playwright:electron:macos -- --last-failed || true yarn test:playwright:electron:macos -- --last-failed || true
else else
echo "Do not run playwright. Unable to detect os runtime." echo "Do not run playwright. Unable to detect os runtime."

View File

@ -362,17 +362,6 @@ jobs:
- name: List artifacts - name: List artifacts
run: "ls -R out" run: "ls -R out"
- name: Set more complete nightly release notes
if: ${{ env.IS_NIGHTLY == 'true' }}
run: |
# Note: prefered going this way instead of a full clone in the checkout step,
# see https://github.com/actions/checkout/issues/1471
git fetch --prune --unshallow --tags
export TAG="nightly-${VERSION}"
export PREVIOUS_TAG=$(git describe --tags --match="nightly-v[0-9]*" --abbrev=0)
export NOTES=$(./scripts/get-nightly-changelog.sh)
yarn files:set-notes
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_NIGHTLY == 'true' }}
uses: 'google-github-actions/auth@v2.1.7' uses: 'google-github-actions/auth@v2.1.7'
@ -394,13 +383,12 @@ jobs:
parent: false parent: false
destination: 'dl.kittycad.io/releases/modeling-app/nightly' destination: 'dl.kittycad.io/releases/modeling-app/nightly'
- name: Tag nightly commit - name: Create draft release
if: ${{ env.IS_NIGHTLY == 'true' }} uses: softprops/action-gh-release@v2
uses: actions/github-script@v7 if: ${{ env.IS_RELEASE == 'true' }}
with: with:
script: | name: ${{ env.VERSION }}
const { VERSION } = process.env tag_name: ${{ env.VERSION }}
const { owner, repo } = context.repo draft: true
const { sha } = context generate_release_notes: true
const ref = `refs/tags/nightly-${VERSION}` files: 'out/Zoo*'
github.rest.git.createRef({ owner, repo, sha, ref })

View File

@ -1,7 +1,7 @@
name: E2E Tests name: E2E Tests
on: on:
push: push:
branches: [ main, pierremtb/move-tests-to-electron ] branches: [ main ]
pull_request: pull_request:
branches: [ main ] branches: [ main ]
@ -33,13 +33,13 @@ jobs:
rust: rust:
- 'src/wasm-lib/**' - 'src/wasm-lib/**'
electron: browser:
timeout-minutes: ${{ matrix.os == 'macos-14' && 60 || 50 }} timeout-minutes: ${{ matrix.os == 'macos-14' && 60 || 50 }}
name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} name: playwright:browser:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest-8-cores, windows-latest-8-cores, macos-14-large] os: [ubuntu-latest-8-cores, windows-latest-8-cores]
shardIndex: [1, 2, 3, 4] shardIndex: [1, 2, 3, 4]
shardTotal: [4] shardTotal: [4]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -68,7 +68,7 @@ jobs:
- name: Download Wasm Cache - name: Download Wasm Cache
id: download-wasm id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false' if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v7 uses: dawidd6/action-download-artifact@v6
continue-on-error: true continue-on-error: true
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
@ -123,13 +123,13 @@ jobs:
if: steps.download-wasm.outcome == 'failure' if: steps.download-wasm.outcome == 'failure'
shell: bash shell: bash
run: yarn build:wasm run: yarn build:wasm
- name: build electron - name: build web
run: yarn build:local
shell: bash shell: bash
run: yarn tron:package
- name: Run ubuntu/chrome snapshots - name: Run ubuntu/chrome snapshots
shell: bash shell: bash
run: | run: |
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env: env:
CI: true CI: true
NODE_ENV: development NODE_ENV: development
@ -186,12 +186,12 @@ jobs:
with: with:
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run playwright/electron flow (with retries) - name: Run playwright/chrome flow (with retries)
id: retry id: retry
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
shell: bash shell: bash
run: | run: |
.github/ci-cd-scripts/playwright-electron.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{matrix.os}} .github/ci-cd-scripts/playwright-browser-chrome.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{matrix.os}}
env: env:
CI: true CI: true
FAIL_ON_CONSOLE_ERRORS: true FAIL_ON_CONSOLE_ERRORS: true
@ -199,6 +199,11 @@ jobs:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true VITE_KC_SKIP_AUTH: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: send to axiom
if: always()
shell: bash
run: |
node playwrightProcess.mjs | tee /tmp/github-actions.log
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
@ -216,3 +221,136 @@ jobs:
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
electron:
name: playwright:electron:${{matrix.os}}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest-8-cores, windows-latest-8-cores, macos-14-large]
timeout-minutes: 60
runs-on: ${{ matrix.os }}
needs: check-rust-changes
steps:
- name: Tune GitHub-hosted runner network
uses: smorimoto/tune-github-hosted-runner-network@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- uses: KittyCAD/action-install-cli@main
- name: Install dependencies
shell: bash
run: yarn
- name: Cache Playwright Browsers
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright/
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
- name: Install Playwright Browsers
shell: bash
run: yarn playwright install chromium --with-deps
- name: Download Wasm Cache
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v6
continue-on-error: true
with:
github_token: ${{secrets.GITHUB_TOKEN}}
name: wasm-bundle
workflow: build-and-store-wasm.yml
branch: main
path: src/wasm-lib/pkg
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
shell: bash
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: OR Cache Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: install good sed
if: ${{ startsWith(matrix.os, 'macos') }}
shell: bash
run: |
brew install gnu-sed
echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
- name: Install vector
if: ${{ startsWith(matrix.os, 'ubuntu') }}
shell: bash
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
shell: bash
run: yarn build:wasm
- name: OR Build Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
shell: bash
run: yarn build:wasm
- name: build electron
shell: bash
run: yarn tron:package
- uses: actions/download-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true
with:
name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
path: test-results/
- name: Run electron tests (with retries)
id: retry
if: ${{ !cancelled() && (success() || failure()) }}
shell: bash
run: |
.github/ci-cd-scripts/playwright-electron.sh ${{ matrix.os }}
env:
CI: true
FAIL_ON_CONSOLE_ERRORS: true
NODE_ENV: development
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true
IS_UBUNTU: ${{ startsWith(matrix.os, 'ubuntu') && 'true' || 'false' }}
#DEBUG: 'pw:browser*'
- name: send to axiom
if: ${{ !cancelled() && (success() || failure()) && !startsWith(matrix.os, 'windows') }}
shell: bash
run: |
node playwrightProcess.mjs | tee /tmp/github-actions.log
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
with:
name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
path: test-results/
include-hidden-files: true
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
with:
name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }}
path: playwright-report/
include-hidden-files: true
retention-days: 30
overwrite: true

View File

@ -132,12 +132,6 @@ jobs:
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async
- name: Upload release files to Github
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2
with:
files: 'out/Zoo*'
announce_release: announce_release:
needs: [publish-apps-release] needs: [publish-apps-release]

1
.gitignore vendored
View File

@ -61,7 +61,6 @@ Mac_App_Distribution.provisionprofile
*.tsbuildinfo *.tsbuildinfo
src/wasm-lib/pkg src/wasm-lib/pkg
.eslintcache
venv venv
.vite/ .vite/

View File

@ -99,7 +99,7 @@ yarn tron:start
This will start the application and hot-reload on changes. This will start the application and hot-reload on changes.
Devtools can be opened with the usual Cmd-Opt-I (Mac) or Ctrl-Shift-I (Linux and Windows). Devtools can be opened with the usual Cmd/Ctrl-Shift-I.
To build, run `yarn tron:package`. To build, run `yarn tron:package`.
@ -136,7 +136,7 @@ https://github.com/KittyCAD/modeling-app/issues/new
#### 2. Push a new tag #### 2. Push a new tag
Create a new tag and push it to the repo. The `semantic-release.sh` script will automatically bump the minor part, which we use the most. For instance going from `v0.27.0` to `v0.28.0`. Create a new tag and push it to the repo (eg. `v0.28.0` for `$VERSION`)
``` ```
VERSION=$(./scripts/semantic-release.sh) VERSION=$(./scripts/semantic-release.sh)
@ -146,14 +146,16 @@ git push origin --tags
This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files as well as updater-test artifacts. This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files as well as updater-test artifacts.
The workflow should be listed right away [in this list](https://github.com/KittyCAD/modeling-app/actions/workflows/build-apps.yml?query=event%3Apush)). Once the workflow succeeds, a draft release will be created at https://github.com/KittyCAD/modeling-app/releases.
#### 3. Manually test artifacts #### 3. Manually test artifacts from the Cut Release PR
##### Release builds ##### Release builds
The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.). The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.).
Alternatively, the draft release will also include these builds.
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue. Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue.
##### Updater-test builds ##### Updater-test builds
@ -176,11 +178,9 @@ If the prompt doesn't show up, start the app in command line to grab the electro
#### 4. Publish the release #### 4. Publish the release
Head over to https://github.com/KittyCAD/modeling-app/releases/new, pick the newly created tag and type it in the _Release title_ field as well. Head over to https://github.com/KittyCAD/modeling-app/releases, paste in the changelog discussed in the issue, and publish the draft release created by the `build-apps` workflow from step 2.
Hit _Generate release notes_ as a starting point to discuss the changelog in the issue. Once done, make sure _Set as the latest release_ is checked, and hit _Publish release_. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter. On success, the files will be uploaded to the public bucket and the announcement on Discord will be sent.
A new `publish-apps-release` will kick in and you should be able to find it [here](https://github.com/KittyCAD/modeling-app/actions?query=event%3Arelease). On success, the files will be uploaded to the public bucket as well as to the GitHub release, and the announcement on Discord will be sent.
#### 5. Close the issue #### 5. Close the issue
@ -388,6 +388,23 @@ yarn test:unit:local
#### E2E Tests #### E2E Tests
**Playwright Browser**
These E2E tests run in a browser (without electron).
There are tests that are skipped if they are ran in a windows OS or Linux OS. We can use playwright tags to implement test skipping.
Breaking down the command `yarn test:playwright:browser:chrome:windows`
- The application is `playwright`
- The runtime is a `browser`
- The specific `browser` is `chrome`
- The test should run in a `windows` environment. It will skip tests that are broken or flaky in the windows OS.
```
yarn test:playwright:browser:chrome
yarn test:playwright:browser:chrome:windows
yarn test:playwright:browser:chrome:ubuntu
```
**Playwright Electron** **Playwright Electron**
These E2E tests run in electron. There are tests that are skipped if they are ran in a windows, linux, or macos environment. We can use playwright tags to implement test skipping. These E2E tests run in electron. There are tests that are skipped if they are ran in a windows, linux, or macos environment. We can use playwright tags to implement test skipping.
@ -433,9 +450,3 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
## KCL ## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl). For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).
### Logging
To display logging (to the terminal or console) set `ZOO_LOG=1`. This will log some warnings and simple performance metrics. To view these in test runs, use `-- --nocapture`.
To enable memory metrics, build with `--features dhat-heap`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -58,7 +58,7 @@ mountingPlate = extrude(thickness, mountingPlateSketch)
```js ```js
// Sketch on the face of a chamfer. // Sketch on the face of a chamfer.
fn cube(pos, scale) { fn cube = (pos, scale) => {
sg = startSketchOn('XY') sg = startSketchOn('XY')
|> startProfileAt(pos, %) |> startProfileAt(pos, %)
|> line([0, scale], %) |> line([0, scale], %)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -37,7 +37,7 @@ assertEqual(n, 3, 0.0001, "5/2 = 2.5, rounded up makes 3")
startSketchOn('XZ') startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 2 }, %) |> circle({ center = [0, 0], radius = 2 }, %)
|> extrude(5, %) |> extrude(5, %)
|> patternTransform(n, fn(id) { |> patternTransform(n, (id) => {
return { translate = [4 * id, 0, 0] } return { translate = [4 * id, 0, 0] }
}, %) }, %)
``` ```

View File

@ -29,7 +29,7 @@ map(array: [KclValue], map_fn: FunctionParam) -> [KclValue]
```js ```js
r = 10 // radius r = 10 // radius
fn drawCircle(id) { fn drawCircle = (id) => {
return startSketchOn("XY") return startSketchOn("XY")
|> circle({ center = [id * 2 * r, 0], radius = r }, %) |> circle({ center = [id * 2 * r, 0], radius = r }, %)
} }
@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
```js ```js
r = 10 // radius r = 10 // radius
// Call `map`, using an anonymous function instead of a named one. // Call `map`, using an anonymous function instead of a named one.
circles = map([1..3], (id) { circles = map([1..3], (id) => {
return startSketchOn("XY") return startSketchOn("XY")
|> circle({ center = [id * 2 * r, 0], radius = r }, %) |> circle({ center = [id * 2 * r, 0], radius = r }, %)
}) })

View File

@ -12,7 +12,7 @@ to other modules.
``` ```
// util.kcl // util.kcl
export fn increment(x) { export fn increment = (x) => {
return x + 1 return x + 1
} }
``` ```
@ -37,11 +37,11 @@ Multiple functions can be exported in a file.
``` ```
// util.kcl // util.kcl
export fn increment(x) { export fn increment = (x) => {
return x + 1 return x + 1
} }
export fn decrement(x) { export fn decrement = (x) => {
return x - 1 return x - 1
} }
``` ```

File diff suppressed because one or more lines are too long

View File

@ -30,7 +30,7 @@ patternTransform2d(total_instances: u32, transform_function: FunctionParam, soli
```js ```js
// Each instance will be shifted along the X axis. // Each instance will be shifted along the X axis.
fn transform(id) { fn transform = (id) => {
return { translate = [4 * id, 0] } return { translate = [4 * id, 0] }
} }

View File

@ -30,14 +30,14 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
```js ```js
// This function adds two numbers. // This function adds two numbers.
fn add(a, b) { fn add = (a, b) => {
return a + b return a + b
} }
// This function adds an array of numbers. // This function adds an array of numbers.
// It uses the `reduce` function, to call the `add` function on every // It uses the `reduce` function, to call the `add` function on every
// element of the `arr` parameter. The starting value is 0. // element of the `arr` parameter. The starting value is 0.
fn sum(arr) { fn sum = (arr) => {
return reduce(arr, 0, add) return reduce(arr, 0, add)
} }
@ -61,7 +61,7 @@ assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
// an anonymous `add` function as its parameter, instead of declaring a // an anonymous `add` function as its parameter, instead of declaring a
// named function outside. // named function outside.
arr = [1, 2, 3] arr = [1, 2, 3]
sum = reduce(arr, 0, (i, result_so_far) { sum = reduce(arr, 0, (i, result_so_far) => {
return i + result_so_far return i + result_so_far
}) })
@ -74,7 +74,7 @@ assertEqual(sum, 6, 0.00001, "1 + 2 + 3 summed is 6")
```js ```js
// Declare a function that sketches a decagon. // Declare a function that sketches a decagon.
fn decagon(radius) { fn decagon = (radius) => {
// Each side of the decagon is turned this many degrees from the previous angle. // Each side of the decagon is turned this many degrees from the previous angle.
stepAngle = 1 / 10 * tau() stepAngle = 1 / 10 * tau()
@ -84,7 +84,7 @@ fn decagon(radius) {
// Use a `reduce` to draw the remaining decagon sides. // Use a `reduce` to draw the remaining decagon sides.
// For each number in the array 1..10, run the given function, // For each number in the array 1..10, run the given function,
// which takes a partially-sketched decagon and adds one more edge to it. // which takes a partially-sketched decagon and adds one more edge to it.
fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) { fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) => {
// Draw one edge of the decagon. // Draw one edge of the decagon.
x = cos(stepAngle * i) * radius x = cos(stepAngle * i) * radius
y = sin(stepAngle * i) * radius y = sin(stepAngle * i) * radius

File diff suppressed because one or more lines are too long

View File

@ -36,7 +36,7 @@ cube = startSketchAt([0, 0])
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
fn cylinder(radius, tag) { fn cylinder = (radius, tag) => {
return startSketchAt([0, 0]) return startSketchAt([0, 0])
|> circle({ |> circle({
radius = radius, radius = radius,

View File

@ -36,7 +36,7 @@ cube = startSketchAt([0, 0])
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
fn cylinder(radius, tag) { fn cylinder = (radius, tag) => {
return startSketchAt([0, 0]) return startSketchAt([0, 0])
|> circle({ |> circle({
radius = radius, radius = radius,

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,7 @@ If you want to get a value from an array you can use the index like so:
An object is defined with `{}` braces. Here is an example object: An object is defined with `{}` braces. Here is an example object:
``` ```
myObj = { a = 0, b = "thing" } myObj = {a: 0, b: "thing"}
``` ```
We support two different ways of getting properties from objects, you can call We support two different ways of getting properties from objects, you can call
@ -54,7 +54,7 @@ We also have support for defining your own functions. Functions can take in any
type of argument. Below is an example of the syntax: type of argument. Below is an example of the syntax:
``` ```
fn myFn(x) { fn myFn = (x) => {
return x return x
} }
``` ```
@ -90,12 +90,12 @@ startSketchOn('XZ')
|> startProfileAt(origin, %) |> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001) |> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001) - 90, segAng(rectangleSegmentA001, %) - 90,
196.99 196.99
], %, $rectangleSegmentB001) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001), segAng(rectangleSegmentA001, %),
-segLen(rectangleSegmentA001) -segLen(rectangleSegmentA001, %)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
@ -118,20 +118,20 @@ use the tag `rectangleSegmentA001` in any function or expression in the file.
However if the code was written like this: However if the code was written like this:
``` ```
fn rect(origin) { fn rect = (origin) => {
return startSketchOn('XZ') return startSketchOn('XZ')
|> startProfileAt(origin, %) |> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001) |> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001) - 90, segAng(rectangleSegmentA001, %) - 90,
196.99 196.99
], %, $rectangleSegmentB001) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001), segAng(rectangleSegmentA001, %),
-segLen(rectangleSegmentA001) -segLen(rectangleSegmentA001, %)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
} }
rect([0, 0]) rect([0, 0])
@ -146,31 +146,28 @@ Tags are accessible through the sketch group they are declared in.
For example the following code works. For example the following code works.
``` ```
fn rect(origin) { fn rect = (origin) => {
return startSketchOn('XZ') return startSketchOn('XZ')
|> startProfileAt(origin, %) |> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001) |> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001) - 90, segAng(rectangleSegmentA001, %) - 90,
196.99 196.99
], %, $rectangleSegmentB001) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001), segAng(rectangleSegmentA001, %),
-segLen(rectangleSegmentA001) -segLen(rectangleSegmentA001, %)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
} }
rect([0, 0]) rect([0, 0])
myRect = rect([20, 0]) myRect = rect([20, 0])
myRect myRect
|> extrude(10, %) |> extrude(10, %)
|> fillet({ |> fillet({radius: 0.5, tags: [myRect.tags.rectangleSegmentA001]}, %)
radius = 0.5,
tags = [myRect.tags.rectangleSegmentA001]
}, %)
``` ```
See how we use the tag `rectangleSegmentA001` in the `fillet` function outside See how we use the tag `rectangleSegmentA001` in the `fillet` function outside

View File

@ -0,0 +1,161 @@
---
title: "BinaryOperator"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Add two numbers.
**enum:** `+`
----
Subtract two numbers.
**enum:** `-`
----
Multiply two numbers.
**enum:** `*`
----
Divide two numbers.
**enum:** `/`
----
Modulo two numbers.
**enum:** `%`
----
Raise a number to a power.
**enum:** `^`
----
Are two numbers equal?
**enum:** `==`
----
Are two numbers not equal?
**enum:** `!=`
----
Is left greater than right
**enum:** `>`
----
Is left greater than or equal to right
**enum:** `>=`
----
Is left less than right
**enum:** `<`
----
Is left less than or equal to right
**enum:** `<=`
----

View File

@ -0,0 +1,161 @@
---
title: "BinaryPart"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `BinaryExpression`| | No |
| `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| | No |
| `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CallExpression`| | No |
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `optional` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UnaryExpression`| | No |
| `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| | No |
| `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `IfExpression`| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| | No |
| `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`| | No |
| `final_else` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -0,0 +1,97 @@
---
title: "BodyItem"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ImportStatement`| | No |
| `items` |`[` [`ImportItem`](/docs/kcl/types/ImportItem) `]`| | No |
| `path` |`string`| | No |
| `raw_path` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ExpressionStatement`| | No |
| `expression` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `VariableDeclaration`| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ReturnStatement`| | No |
| `argument` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -0,0 +1,41 @@
---
title: "CommentStyle"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Like // foo
**enum:** `line`
----
Like /* foo */
**enum:** `block`
----

24
docs/kcl/types/ElseIf.md Normal file
View File

@ -0,0 +1,24 @@
---
title: "ElseIf"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `cond` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,16 @@
---
title: "EnvironmentRef"
excerpt: "An index pointing to an environment."
layout: manual
---
An index pointing to an environment.
**Type:** `integer` (`uint`)

318
docs/kcl/types/Expr.md Normal file
View File

@ -0,0 +1,318 @@
---
title: "Expr"
excerpt: "An expression can be evaluated to yield a single KCL value."
layout: manual
---
An expression can be evaluated to yield a single KCL value.
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| An expression can be evaluated to yield a single KCL value. | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`TagDeclarator`](/docs/kcl/types#tag-declaration)| | No |
| `value` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `BinaryExpression`| | No |
| `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| An expression can be evaluated to yield a single KCL value. | No |
| `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`FunctionExpression`](/docs/kcl/types/FunctionExpression)| | No |
| `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`| | No |
| `body` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CallExpression`| | No |
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| An expression can be evaluated to yield a single KCL value. | No |
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `optional` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `PipeExpression`| | No |
| `body` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `PipeSubstitution`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayExpression`| | No |
| `elements` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayRangeExpression`| | No |
| `startElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endInclusive` |`boolean`| Is the `end_element` included in the range? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ObjectExpression`| | No |
| `properties` |`[` [`ObjectProperty`](/docs/kcl/types/ObjectProperty) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| An expression can be evaluated to yield a single KCL value. | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| An expression can be evaluated to yield a single KCL value. | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UnaryExpression`| | No |
| `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| An expression can be evaluated to yield a single KCL value. | No |
| `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `IfExpression`| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`| | No |
| `final_else` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application).
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `None`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -0,0 +1,24 @@
---
title: "FunctionExpression"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`| | No |
| `body` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,23 @@
---
title: "Identifier"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,24 @@
---
title: "ImportItem"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `name` |[`Identifier`](/docs/kcl/types/Identifier)| Name of the item to import. | No |
| `alias` |[`Identifier`](/docs/kcl/types/Identifier)| Rename the item using an identifier after "as". | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,16 @@
---
title: "ItemVisibility"
excerpt: ""
layout: manual
---
**enum:** `default`, `export`

View File

@ -317,6 +317,7 @@ Data for an imported geometry.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Function`| | No | | `type` |enum: `Function`| | No |
| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| Any KCL value. | No |
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No | | `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -0,0 +1,56 @@
---
title: "LiteralIdentifier"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -0,0 +1,47 @@
---
title: "LiteralValue"
excerpt: ""
layout: manual
---
**This schema accepts any of the following:**
**Type:** `number` (`double`)
----
**Type:** `string`
----
**Type:** `boolean`
----

View File

@ -0,0 +1,57 @@
---
title: "MemberObject"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -0,0 +1,22 @@
---
title: "NonCodeMeta"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `nonCodeNodes` |`object`| | No |
| `startNodes` |`[` [`NonCodeNode`](/docs/kcl/types/NonCodeNode) `]`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -0,0 +1,23 @@
---
title: "NonCodeNode"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `value` |[`NonCodeValue`](/docs/kcl/types/NonCodeValue)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,86 @@
---
title: "NonCodeValue"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `inlineComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
A block comment. An example of this is the following: ```python,no_run /* This is a block comment */ 1 + 1 ``` Now this is important. The block comment is attached to the next line. This is always the case. Also the block comment doesn't have a new line above it. If it did it would be a `NewLineBlockComment`.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `blockComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
A block comment that has a new line above it. The user explicitly added a new line above the block comment.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `newLineBlockComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `newLine`| | No |
----

View File

@ -0,0 +1,24 @@
---
title: "ObjectProperty"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `key` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
| `value` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,23 @@
---
title: "Parameter"
excerpt: "Parameter of a KCL function."
layout: manual
---
Parameter of a KCL function.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `identifier` |[`Identifier`](/docs/kcl/types/Identifier)| The parameter's label or name. | No |
| `optional` |`boolean`| Is the parameter optional? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

26
docs/kcl/types/Program.md Normal file
View File

@ -0,0 +1,26 @@
---
title: "Program"
excerpt: "A KCL program top level, or function body."
layout: manual
---
A KCL program top level, or function body.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `body` |`[` [`BodyItem`](/docs/kcl/types/BodyItem) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| A KCL program top level, or function body. | No |
| `shebang` |[`Shebang`](/docs/kcl/types/Shebang)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

23
docs/kcl/types/Shebang.md Normal file
View File

@ -0,0 +1,23 @@
---
title: "Shebang"
excerpt: "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```"
layout: manual
---
A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `content` |`string`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

15
docs/kcl/types/Uint.md Normal file
View File

@ -0,0 +1,15 @@
---
title: "Uint"
excerpt: ""
layout: manual
---
**Type:** `integer` (`uint32`)

View File

@ -0,0 +1,41 @@
---
title: "UnaryOperator"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Negate a number.
**enum:** `-`
----
Negate a boolean.
**enum:** `!`
----

View File

@ -0,0 +1,24 @@
---
title: "VariableDeclarator"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `id` |[`Identifier`](/docs/kcl/types/Identifier)| The identifier of the variable. | No |
| `init` |[`Expr`](/docs/kcl/types/Expr)| The value of the variable. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,41 @@
---
title: "VariableKind"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Declare a named constant.
**enum:** `const`
----
Declare a function.
**enum:** `fn`
----

View File

@ -1,11 +1,22 @@
import { test, expect } from './zoo-test' import { test, expect } from '@playwright/test'
import { setupElectron, tearDown } from './test-utils'
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Electron app header tests', () => { test.describe('Electron app header tests', () => {
test( test(
'Open Command Palette button has correct shortcut', 'Open Command Palette button has correct shortcut',
{ tag: '@electron' }, { tag: '@electron' },
async ({ page }, testInfo) => { async ({ browserName }, testInfo) => {
await page.setBodyDimensions({ width: 1200, height: 500 }) const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
await page.setViewportSize({ width: 1200, height: 500 })
// No space before the shortcut since it checks textContent. // No space before the shortcut since it checks textContent.
let text let text
@ -23,14 +34,21 @@ test.describe('Electron app header tests', () => {
const commandsButton = page.getByRole('button', { name: 'Commands' }) const commandsButton = page.getByRole('button', { name: 'Commands' })
await expect(commandsButton).toBeVisible() await expect(commandsButton).toBeVisible()
await expect(commandsButton).toHaveText(text) await expect(commandsButton).toHaveText(text)
await electronApp.close()
} }
) )
test( test(
'User settings has correct shortcut', 'User settings has correct shortcut',
{ tag: '@electron' }, { tag: '@electron' },
async ({ page }, testInfo) => { async ({ browserName }, testInfo) => {
await page.setBodyDimensions({ width: 1200, height: 500 }) const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
await page.setViewportSize({ width: 1200, height: 500 })
// Open the user sidebar menu. // Open the user sidebar menu.
await page.getByTestId('user-sidebar-toggle').click() await page.getByTestId('user-sidebar-toggle').click()
@ -41,6 +59,8 @@ test.describe('Electron app header tests', () => {
const userSettingsButton = page.getByTestId('user-settings') const userSettingsButton = page.getByTestId('user-settings')
await expect(userSettingsButton).toBeVisible() await expect(userSettingsButton).toBeVisible()
await expect(userSettingsButton).toHaveText(text) await expect(userSettingsButton).toHaveText(text)
await electronApp.close()
} }
) )
}) })

View File

@ -1,26 +1,29 @@
import { test, expect, Page } from './zoo-test' import { test, expect, Page } from '@playwright/test'
import { import {
getUtils, getUtils,
TEST_COLORS, TEST_COLORS,
setup,
tearDown,
commonPoints, commonPoints,
PERSIST_MODELING_CONTEXT, PERSIST_MODELING_CONTEXT,
} from './test-utils' } from './test-utils'
import { HomePageFixture } from './fixtures/homePageFixture'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.setTimeout(120000) test.setTimeout(120000)
async function doBasicSketch( async function doBasicSketch(page: Page, openPanes: string[]) {
page: Page,
homePage: HomePageFixture,
openPanes: string[]
) {
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.waitForPageLoad()
await page.waitForTimeout()
await u.openDebugPanel() await u.openDebugPanel()
// If we have the code pane open, we should see the code. // If we have the code pane open, we should see the code.
@ -145,11 +148,13 @@ async function doBasicSketch(
} }
test.describe('Basic sketch', () => { test.describe('Basic sketch', () => {
test.fixme('code pane open at start', async ({ page, homePage }) => { test('code pane open at start', { tag: ['@skipWin'] }, async ({ page }) => {
await doBasicSketch(page, homePage, ['code']) // Skip on windows it is being weird.
test.skip(process.platform === 'win32', 'Skip on windows')
await doBasicSketch(page, ['code'])
}) })
test.fixme('code pane closed at start', async ({ page, homePage }) => { test('code pane closed at start', async ({ page }) => {
// Load the app with the code panes // Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => { await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem( localStorage.setItem(
@ -157,6 +162,6 @@ test.describe('Basic sketch', () => {
JSON.stringify({ openPanes: [] }) JSON.stringify({ openPanes: [] })
) )
}, PERSIST_MODELING_CONTEXT) }, PERSIST_MODELING_CONTEXT)
await doBasicSketch(page, homePage, []) await doBasicSketch(page, [])
}) })
}) })

View File

@ -1,21 +1,27 @@
import { test, expect, Page } from './zoo-test' import { test, expect } from '@playwright/test'
import { HomePageFixture } from './fixtures/homePageFixture' import { getUtils, setup, tearDown } from './test-utils'
import { getUtils } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Can create sketches on all planes and their back sides', () => { test.describe('Can create sketches on all planes and their back sides', () => {
const sketchOnPlaneAndBackSideTest = async ( const sketchOnPlaneAndBackSideTest = async (
page: Page, page: any,
homePage: HomePageFixture,
plane: string, plane: string,
clickCoords: { x: number; y: number } clickCoords: { x: number; y: number }
) => { ) => {
const u = await getUtils(page) const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
const coord = const coord =
@ -77,39 +83,32 @@ test.describe('Can create sketches on all planes and their back sides', () => {
await u.clearCommandLogs() await u.clearCommandLogs()
await u.removeCurrentCode() await u.removeCurrentCode()
} }
test('XY', async ({ page, homePage }) => { test('XY', async ({ page }) => {
await sketchOnPlaneAndBackSideTest( await sketchOnPlaneAndBackSideTest(
page, page,
homePage,
'XY', 'XY',
{ x: 600, y: 388 } // red plane { x: 600, y: 388 } // red plane
// { x: 600, y: 400 }, // red plane // clicks grid helper and that causes problems, should fix so that these coords work too. // { x: 600, y: 400 }, // red plane // clicks grid helper and that causes problems, should fix so that these coords work too.
) )
}) })
test('YZ', async ({ page, homePage }) => { test('YZ', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, 'YZ', { x: 700, y: 250 }) // green plane await sketchOnPlaneAndBackSideTest(page, 'YZ', { x: 700, y: 250 }) // green plane
}) })
test('XZ', async ({ page, homePage }) => { test('XZ', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, '-XZ', { x: 700, y: 80 }) // blue plane await sketchOnPlaneAndBackSideTest(page, '-XZ', { x: 700, y: 80 }) // blue plane
}) })
test('-XY', async ({ page, homePage }) => { test('-XY', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, '-XY', { await sketchOnPlaneAndBackSideTest(page, '-XY', { x: 600, y: 118 }) // back of red plane
x: 600,
y: 118,
}) // back of red plane
}) })
test('-YZ', async ({ page, homePage }) => { test('-YZ', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, '-YZ', { await sketchOnPlaneAndBackSideTest(page, '-YZ', { x: 700, y: 219 }) // back of green plane
x: 700,
y: 219,
}) // back of green plan
}) })
test('-XZ', async ({ page, homePage }) => { test('-XZ', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, 'XZ', { x: 700, y: 427 }) // back of blue plane await sketchOnPlaneAndBackSideTest(page, 'XZ', { x: 700, y: 427 }) // back of blue plane
}) })
}) })

View File

@ -1,15 +1,28 @@
import { test, expect } from './zoo-test' import { test, expect } from '@playwright/test'
import { getUtils, executorInputPath } from './test-utils' import {
getUtils,
setup,
setupElectron,
tearDown,
executorInputPath,
} from './test-utils'
import { join } from 'path' import { join } from 'path'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates' import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates'
import fsp from 'fs/promises' import fsp from 'fs/promises'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Code pane and errors', () => { test.describe('Code pane and errors', () => {
test('Typing KCL errors induces a badge on the code pane button', async ({ test('Typing KCL errors induces a badge on the code pane button', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -18,18 +31,18 @@ test.describe('Code pane and errors', () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`// Extruded Triangle `// Extruded Triangle
sketch001 = startSketchOn('XZ') sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([10, 0], %) |> line([10, 0], %)
|> line([-5, 10], %) |> line([-5, 10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(5, sketch001)` extrude001 = extrude(5, sketch001)`
) )
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -51,9 +64,9 @@ test.describe('Code pane and errors', () => {
test('Opening and closing the code pane will consistently show error diagnostics', async ({ test('Opening and closing the code pane will consistently show error diagnostics', async ({
page, page,
homePage,
editor,
}) => { }) => {
await page.goto('http://localhost:3000')
const u = await getUtils(page) const u = await getUtils(page)
// Load the app with the working starter code // Load the app with the working starter code
@ -61,8 +74,8 @@ test.describe('Code pane and errors', () => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
}, bracket) }, bracket)
await page.setBodyDimensions({ width: 1200, height: 900 }) await page.setViewportSize({ width: 1200, height: 900 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -78,9 +91,8 @@ test.describe('Code pane and errors', () => {
await expect(codePaneButtonHolder).not.toContainText('notification') await expect(codePaneButtonHolder).not.toContainText('notification')
// Delete a character to break the KCL // Delete a character to break the KCL
await editor.openPane() await u.openKclCodePanel()
await editor.scrollToText('thickness, bracketLeg1Sketch)') await page.getByText('thickness, bracketLeg1Sketch)').click()
await page.getByText('extrude(thickness, bracketLeg1Sketch)').click()
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
// Ensure that a badge appears on the button // Ensure that a badge appears on the button
@ -104,10 +116,7 @@ test.describe('Code pane and errors', () => {
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Open the code pane // Open the code pane
await editor.openPane() await u.openKclCodePanel()
// Go to our problematic code again (missing closing paren!)
await editor.scrollToText('extrude(thickness, bracketLeg1Sketch')
// Ensure that a badge appears on the button // Ensure that a badge appears on the button
await expect(codePaneButtonHolder).toContainText('notification') await expect(codePaneButtonHolder).toContainText('notification')
@ -120,18 +129,18 @@ test.describe('Code pane and errors', () => {
await expect(page.locator('.cm-tooltip').first()).toBeVisible() await expect(page.locator('.cm-tooltip').first()).toBeVisible()
}) })
test.fixme('When error is not in view you can click the badge to scroll to it', async ({ test('When error is not in view you can click the badge to scroll to it', async ({
page, page,
homePage,
context,
}) => { }) => {
const u = await getUtils(page)
// Load the app with the working starter code // Load the app with the working starter code
await context.addInitScript((code) => { await page.addInitScript((code) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
}, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW) }, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
@ -155,24 +164,24 @@ test.describe('Code pane and errors', () => {
await expect( await expect(
page page
.getByText( .getByText(
'Modeling command failed: [ApiError { error_code: InternalEngine, message: "Solid3D revolve failed: sketch profile must lie entirely on one side of the revolution axis" }]' 'sketch profile must lie entirely on one side of the revolution axis'
) )
.first() .first()
).toBeVisible() ).toBeVisible()
}) })
test('When error is not in view WITH LINTS you can click the badge to scroll to it', async ({ test('When error is not in view WITH LINTS you can click the badge to scroll to it', async ({
context,
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page)
// Load the app with the working starter code // Load the app with the working starter code
await context.addInitScript((code) => { await page.addInitScript((code) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
}, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW) }, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
@ -232,29 +241,32 @@ test.describe('Code pane and errors', () => {
test( test(
'Opening multiple panes persists when switching projects', 'Opening multiple panes persists when switching projects',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ browserName }, testInfo) => {
// Setup multiple projects. // Setup multiple projects.
await context.folderSetupFn(async (dir) => { const { electronApp, page } = await setupElectron({
const routerTemplateDir = join(dir, 'router-template-slate') testInfo,
const bracketDir = join(dir, 'bracket') folderSetupFn: async (dir) => {
await Promise.all([ const routerTemplateDir = join(dir, 'router-template-slate')
fsp.mkdir(routerTemplateDir, { recursive: true }), const bracketDir = join(dir, 'bracket')
fsp.mkdir(bracketDir, { recursive: true }), await Promise.all([
]) fsp.mkdir(routerTemplateDir, { recursive: true }),
await Promise.all([ fsp.mkdir(bracketDir, { recursive: true }),
fsp.copyFile( ])
executorInputPath('router-template-slate.kcl'), await Promise.all([
join(routerTemplateDir, 'main.kcl') fsp.copyFile(
), executorInputPath('router-template-slate.kcl'),
fsp.copyFile( join(routerTemplateDir, 'main.kcl')
executorInputPath('focusrite_scarlett_mounting_braket.kcl'), ),
join(bracketDir, 'main.kcl') fsp.copyFile(
), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
]) join(bracketDir, 'main.kcl')
),
])
},
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await test.step('Opening the bracket project should load', async () => { await test.step('Opening the bracket project should load', async () => {
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
@ -297,21 +309,30 @@ test(
await expect(page.locator('#variables-pane')).toBeVisible() await expect(page.locator('#variables-pane')).toBeVisible()
await expect(page.locator('#logs-pane')).toBeVisible() await expect(page.locator('#logs-pane')).toBeVisible()
}) })
await electronApp.close()
} }
) )
test( test(
'external change of file contents are reflected in editor', 'external change of file contents are reflected in editor',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ browserName }, testInfo) => {
const PROJECT_DIR_NAME = 'lee-was-here' const PROJECT_DIR_NAME = 'lee-was-here'
const { dir: projectsDir } = await context.folderSetupFn(async (dir) => { const {
const aProjectDir = join(dir, PROJECT_DIR_NAME) electronApp,
await fsp.mkdir(aProjectDir, { recursive: true }) page,
dir: projectsDir,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const aProjectDir = join(dir, PROJECT_DIR_NAME)
await fsp.mkdir(aProjectDir, { recursive: true })
},
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await test.step('Open the project', async () => { await test.step('Open the project', async () => {
await expect(page.getByText(PROJECT_DIR_NAME)).toBeVisible() await expect(page.getByText(PROJECT_DIR_NAME)).toBeVisible()
@ -330,5 +351,7 @@ test(
) )
await u.editorTextMatches(content) await u.editorTextMatches(content)
}) })
await electronApp.close()
} }
) )

View File

@ -1,30 +1,37 @@
import { test, expect } from './zoo-test' import { test, expect } from '@playwright/test'
import { getUtils } from './test-utils' import { getUtils, setup, tearDown } from './test-utils'
import { KCL_DEFAULT_LENGTH } from 'lib/constants' import { KCL_DEFAULT_LENGTH } from 'lib/constants'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Command bar tests', () => { test.describe('Command bar tests', () => {
test('Extrude from command bar selects extrude line after', async ({ test('Extrude from command bar selects extrude line after', async ({
page, page,
homePage,
}) => { }) => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XY') `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> xLine(-20, %) |> xLine(-20, %)
|> close(%) |> close(%)
` `
) )
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -45,24 +52,24 @@ test.describe('Command bar tests', () => {
) )
}) })
test('Fillet from command bar', async ({ page, homePage }) => { test('Fillet from command bar', async ({ page }) => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XY') `sketch001 = startSketchOn('XY')
|> startProfileAt([-5, -5], %) |> startProfileAt([-5, -5], %)
|> line([0, 10], %) |> line([0, 10], %)
|> line([10, 0], %) |> line([10, 0], %)
|> line([0, -10], %) |> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(-10, sketch001)` extrude001 = extrude(-10, sketch001)`
) )
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
@ -86,10 +93,10 @@ test.describe('Command bar tests', () => {
test('Command bar can change a setting, and switch back and forth between arguments', async ({ test('Command bar can change a setting, and switch back and forth between arguments', async ({
page, page,
homePage,
}) => { }) => {
await page.setBodyDimensions({ width: 1200, height: 500 }) const u = await getUtils(page)
await homePage.goToModelingScene() await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
const commandBarButton = page.getByRole('button', { name: 'Commands' }) const commandBarButton = page.getByRole('button', { name: 'Commands' })
const cmdSearchBar = page.getByPlaceholder('Search commands') const cmdSearchBar = page.getByPlaceholder('Search commands')
@ -146,7 +153,7 @@ test.describe('Command bar tests', () => {
// Check that the visibility changed // Check that the visibility changed
await expect(paneSelector).not.toBeVisible() await expect(paneSelector).not.toBeVisible()
commandOptionInput = page.locator('[id="option-input"]') commandOptionInput = page.getByPlaceholder('off')
// Test case for https://github.com/KittyCAD/modeling-app/issues/2882 // Test case for https://github.com/KittyCAD/modeling-app/issues/2882
await commandBarButton.click() await commandBarButton.click()
@ -167,10 +174,10 @@ test.describe('Command bar tests', () => {
test('Command bar keybinding works from code editor and can change a setting', async ({ test('Command bar keybinding works from code editor and can change a setting', async ({
page, page,
homePage,
}) => { }) => {
await page.setBodyDimensions({ width: 1200, height: 500 }) const u = await getUtils(page)
await homePage.goToModelingScene() await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
@ -214,25 +221,25 @@ test.describe('Command bar tests', () => {
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
}) })
test('Can extrude from the command bar', async ({ page, homePage }) => { test('Can extrude from the command bar', async ({ page }) => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`distance = sqrt(20) `distance = sqrt(20)
sketch001 = startSketchOn('XZ') sketch001 = startSketchOn('XZ')
|> startProfileAt([-6.95, 10.98], %) |> startProfileAt([-6.95, 10.98], %)
|> line([25.1, 0.41], %) |> line([25.1, 0.41], %)
|> line([0.73, -20.93], %) |> line([0.73, -20.93], %)
|> line([-23.44, 0.52], %) |> line([-23.44, 0.52], %)
|> close(%) |> close(%)
` `
) )
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// Make sure the stream is up // Make sure the stream is up
await u.openDebugPanel() await u.openDebugPanel()
@ -286,19 +293,26 @@ test.describe('Command bar tests', () => {
await continueButton.click() await continueButton.click()
await submitButton.click() await submitButton.click()
// Check that the code was updated
await u.waitForCmdReceive('extrude') await u.waitForCmdReceive('extrude')
// Unfortunately this indentation seems to matter for the test
await expect(page.locator('.cm-content')).toContainText( await expect(page.locator('.cm-content')).toHaveText(
'extrude001 = extrude(distance001, sketch001)' `distance = sqrt(20)
distance001 = ${KCL_DEFAULT_LENGTH}
sketch001 = startSketchOn('XZ')
|> startProfileAt([-6.95, 10.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -20.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
extrude001 = extrude(distance001, sketch001)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
) )
}) })
test('Can switch between sketch tools via command bar', async ({ test('Can switch between sketch tools via command bar', async ({ page }) => {
page, const u = await getUtils(page)
homePage, await page.setViewportSize({ width: 1200, height: 500 })
}) => { await u.waitForAuthSkipAppStart()
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
const sketchButton = page.getByRole('button', { name: 'Start Sketch' }) const sketchButton = page.getByRole('button', { name: 'Start Sketch' })
const cmdBarButton = page.getByRole('button', { name: 'Commands' }) const cmdBarButton = page.getByRole('button', { name: 'Commands' })

View File

@ -1,16 +1,23 @@
import { test, expect } from './zoo-test' import { test, expect } from '@playwright/test'
import { getUtils } from './test-utils' import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Copilot ghost text', () => { test.describe('Copilot ghost text', () => {
// eslint-disable-next-line jest/valid-title // eslint-disable-next-line jest/valid-title
test.skip(true, 'Needs to get covered again') test.skip(true, 'Needs to get covered again')
test('completes code in empty file', async ({ page, homePage }) => { test('completes code in empty file', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -45,13 +52,12 @@ test.describe('Copilot ghost text', () => {
test.skip('copilot disabled in sketch mode no select plane', async ({ test.skip('copilot disabled in sketch mode no select plane', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -95,13 +101,12 @@ test.describe('Copilot ghost text', () => {
test('copilot disabled in sketch mode after selecting plane', async ({ test('copilot disabled in sketch mode after selecting plane', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -179,12 +184,12 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-ghostText')).not.toBeVisible() await expect(page.locator('.cm-ghostText')).not.toBeVisible()
}) })
test('ArrowUp in code rejects the suggestion', async ({ page, homePage }) => { test('ArrowUp in code rejects the suggestion', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -207,15 +212,12 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('ArrowDown in code rejects the suggestion', async ({ test('ArrowDown in code rejects the suggestion', async ({ page }) => {
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -238,15 +240,12 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('ArrowLeft in code rejects the suggestion', async ({ test('ArrowLeft in code rejects the suggestion', async ({ page }) => {
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -269,15 +268,12 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('ArrowRight in code rejects the suggestion', async ({ test('ArrowRight in code rejects the suggestion', async ({ page }) => {
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -300,12 +296,12 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('Enter in code scoots it down', async ({ page, homePage }) => { test('Enter in code scoots it down', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -330,15 +326,12 @@ test.describe('Copilot ghost text', () => {
) )
}) })
test('Ctrl+shift+z in code rejects the suggestion', async ({ test('Ctrl+shift+z in code rejects the suggestion', async ({ page }) => {
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -367,13 +360,12 @@ test.describe('Copilot ghost text', () => {
test('Ctrl+z in code rejects the suggestion and undos the last code', async ({ test('Ctrl+z in code rejects the suggestion and undos the last code', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await page.waitForTimeout(800) await page.waitForTimeout(800)
await u.codeLocator.click() await u.codeLocator.click()
@ -428,107 +420,98 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
// TODO when we make codemirror a widget, we can test this. // TODO when we make codemirror a widget, we can test this.
//await expect(page.locator('.cm-content')).toHaveText(``) }) //await expect(page.locator('.cm-content')).toHaveText(``)
})
test('delete in code rejects the suggestion', async ({ test('delete in code rejects the suggestion', async ({ page }) => {
page, const u = await getUtils(page)
homePage, // const PUR = 400 / 37.5 //pixeltoUnitRatio
}) => { await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible() await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500) await page.waitForTimeout(500)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible() await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)` `fn cube = (pos, scale) => { sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
) )
await expect(page.locator('.cm-ghostText').first()).toHaveText( await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {` `fn cube = (pos, scale) => {`
) )
// Going elsewhere in the code should hide the ghost text. // Going elsewhere in the code should hide the ghost text.
await page.keyboard.press('Delete') await page.keyboard.press('Delete')
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('backspace in code rejects the suggestion', async ({ test('backspace in code rejects the suggestion', async ({ page }) => {
page, const u = await getUtils(page)
homePage, // const PUR = 400 / 37.5 //pixeltoUnitRatio
}) => { await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible() await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500) await page.waitForTimeout(500)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible() await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)` `fn cube = (pos, scale) => { sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
) )
await expect(page.locator('.cm-ghostText').first()).toHaveText( await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {` `fn cube = (pos, scale) => {`
) )
// Going elsewhere in the code should hide the ghost text. // Going elsewhere in the code should hide the ghost text.
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('focus outside code pane rejects the suggestion', async ({ test('focus outside code pane rejects the suggestion', async ({ page }) => {
page, const u = await getUtils(page)
homePage, // const PUR = 400 / 37.5 //pixeltoUnitRatio
}) => { await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible() await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500) await page.waitForTimeout(500)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible() await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)` `fn cube = (pos, scale) => { sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
) )
await expect(page.locator('.cm-ghostText').first()).toHaveText( await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {` `fn cube = (pos, scale) => {`
) )
// Going outside the editor should hide the ghost text. // Going outside the editor should hide the ghost text.
await page.mouse.move(0, 0) await page.mouse.move(0, 0)
await page await page
.getByRole('button', { name: 'Start Sketch' }) .getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' }) .waitFor({ state: 'visible' })
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
})
}) })
}) })

View File

@ -1,6 +1,14 @@
import { test, expect } from './zoo-test' import { test, expect } from '@playwright/test'
import { getUtils } from './test-utils' import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
function countNewlines(input: string): number { function countNewlines(input: string): number {
let count = 0 let count = 0
@ -16,14 +24,13 @@ test.describe('Debug pane', () => {
test('Artifact IDs in the artifact graph are stable across code edits', async ({ test('Artifact IDs in the artifact graph are stable across code edits', async ({
page, page,
context, context,
homePage,
}) => { }) => {
const code = `sketch001 = startSketchOn('XZ') const code = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([1, 1], %) |> line([1, 1], %)
` `
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const tree = page.getByTestId('debug-feature-tree') const tree = page.getByTestId('debug-feature-tree')
const segment = tree.locator('li', { const segment = tree.locator('li', {
@ -32,7 +39,7 @@ test.describe('Debug pane', () => {
}) })
await test.step('Test setup', async () => { await test.step('Test setup', async () => {
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.openKclCodePanel() await u.openKclCodePanel()
await u.openDebugPanel() await u.openDebugPanel()
// Set the code in the code editor. // Set the code in the code editor.

View File

@ -1,31 +1,39 @@
import { test, expect } from './zoo-test' import { test, expect } from '@playwright/test'
import path from 'path' import { join } from 'path'
import { import {
getUtils, getUtils,
setupElectron,
tearDown,
executorInputPath, executorInputPath,
getPlaywrightDownloadDir,
} from './test-utils' } from './test-utils'
import fsp from 'fs/promises' import fsp from 'fs/promises'
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test( test(
'export works on the first try', 'export works on the first try',
{ tag: '@electron' }, { tag: '@electron' },
async ({ page, context }, testInfo) => { async ({ browserName }, testInfo) => {
await context.folderSetupFn(async (dir) => { const { electronApp, page } = await setupElectron({
const bracketDir = path.join(dir, 'bracket') testInfo,
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })]) folderSetupFn: async (dir) => {
await Promise.all([ const bracketDir = join(dir, 'bracket')
fsp.copyFile( await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
executorInputPath('router-template-slate.kcl'), await Promise.all([
path.join(bracketDir, 'other.kcl') fsp.copyFile(
), executorInputPath('router-template-slate.kcl'),
fsp.copyFile( join(bracketDir, 'other.kcl')
executorInputPath('focusrite_scarlett_mounting_braket.kcl'), ),
path.join(bracketDir, 'main.kcl') fsp.copyFile(
), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
]) join(bracketDir, 'main.kcl')
),
])
},
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -85,16 +93,12 @@ test(
await expect(successToastMessage).toBeVisible() await expect(successToastMessage).toBeVisible()
await expect(exportingToastMessage).not.toBeVisible() await expect(exportingToastMessage).not.toBeVisible()
const firstFileFullPath = path.resolve(
getPlaywrightDownloadDir(page),
exportFileName
)
await test.step('Check the export size', async () => { await test.step('Check the export size', async () => {
await expect await expect
.poll( .poll(
async () => { async () => {
try { try {
const outputGltf = await fsp.readFile(firstFileFullPath) const outputGltf = await fsp.readFile(exportFileName)
return outputGltf.byteLength return outputGltf.byteLength
} catch (e) { } catch (e) {
return 0 return 0
@ -103,6 +107,9 @@ test(
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBeGreaterThan(300_000) .toBeGreaterThan(300_000)
// clean up exported file
await fsp.rm(exportFileName)
}) })
}) })
@ -163,16 +170,12 @@ test(
expect(exportingToastMessage).not.toBeVisible(), expect(exportingToastMessage).not.toBeVisible(),
])) ]))
const secondFileFullPath = path.resolve(
getPlaywrightDownloadDir(page),
exportFileName
)
await test.step('Check the export size', async () => { await test.step('Check the export size', async () => {
await expect await expect
.poll( .poll(
async () => { async () => {
try { try {
const outputGltf = await fsp.readFile(secondFileFullPath) const outputGltf = await fsp.readFile(exportFileName)
return outputGltf.byteLength return outputGltf.byteLength
} catch (e) { } catch (e) {
return 0 return 0
@ -181,7 +184,13 @@ test(
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBeGreaterThan(100_000) .toBeGreaterThan(100_000)
// clean up exported file
await fsp.rm(exportFileName)
}) })
await electronApp.close()
}) })
await electronApp.close()
} }
) )

View File

@ -1,4 +1,4 @@
import { test, expect } from './zoo-test' import { test, expect } from '@playwright/test'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { import {
@ -6,27 +6,37 @@ import {
darkModePlaneColorXZ, darkModePlaneColorXZ,
executorInputPath, executorInputPath,
getUtils, getUtils,
setup,
setupElectron,
tearDown,
} from './test-utils' } from './test-utils'
import { join } from 'path' import { join } from 'path'
test.describe('Editor tests', () => { test.beforeEach(async ({ context, page }, testInfo) => {
test('can comment out code with ctrl+/', async ({ page, homePage }) => { await setup(context, page, testInfo)
const u = await getUtils(page) })
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Editor tests', () => {
test('can comment out code with ctrl+/', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.codeLocator.click() await u.codeLocator.click()
await page.keyboard.type(`sketch001 = startSketchOn('XY') await page.keyboard.type(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)`) |> close(%)`)
await page.keyboard.down('ControlOrMeta') await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('/') await page.keyboard.press('/')
@ -34,11 +44,11 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XY') .toHaveText(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
// |> close(%)`) // |> close(%)`)
// uncomment the code // uncomment the code
await page.keyboard.down('ControlOrMeta') await page.keyboard.down('ControlOrMeta')
@ -47,63 +57,61 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XY') .toHaveText(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)`) |> close(%)`)
}) })
test('if you click the format button it formats your code', async ({ test('if you click the format button it formats your code', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.codeLocator.click() await u.codeLocator.click()
await page.keyboard.type(`sketch001 = startSketchOn('XY') await page.keyboard.type(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
await page.locator('#code-pane button:first-child').click()
await page.locator('button:has-text("Format code")').click()
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)`) |> close(%)`)
await page.locator('#code-pane button:first-child').click()
await page.locator('button:has-text("Format code")').click()
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
}) })
test('if you click the format button it formats your code and executes so lints are still there', async ({ test('if you click the format button it formats your code and executes so lints are still there', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.codeLocator.click() await u.codeLocator.click()
await page.keyboard.type(`sketch_001 = startSketchOn('XY') await page.keyboard.type(`sketch_001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)`) |> close(%)`)
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -127,11 +135,11 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch_001 = startSketchOn('XY') .toHaveText(`sketch_001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)`) |> close(%)`)
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
@ -143,27 +151,29 @@ test.describe('Editor tests', () => {
).toBeVisible() ).toBeVisible()
}) })
test('fold gutters work', async ({ page, homePage }) => { test('fold gutters work', async ({ page }) => {
const u = await getUtils(page)
const fullCode = `sketch001 = startSketchOn('XY') const fullCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)` |> close(%)`
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XY') `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)` |> close(%)`
) )
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// TODO: Jess needs to fix this but you have to mod the code to get them to show // TODO: Jess needs to fix this but you have to mod the code to get them to show
// up, its an annoying codemirror thing. // up, its an annoying codemirror thing.
@ -214,25 +224,22 @@ test.describe('Editor tests', () => {
await expect(foldGutterFoldLine).not.toBeVisible() await expect(foldGutterFoldLine).not.toBeVisible()
}) })
test('hover over functions shows function description', async ({ test('hover over functions shows function description', async ({ page }) => {
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XY') `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)` |> close(%)`
) )
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -261,24 +268,23 @@ test.describe('Editor tests', () => {
test('if you use the format keyboard binding it formats your code', async ({ test('if you use the format keyboard binding it formats your code', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XY') `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)` |> close(%)`
) )
localStorage.setItem('disableAxis', 'true') localStorage.setItem('disableAxis', 'true')
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -295,33 +301,32 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XY') .toHaveText(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)`) |> close(%)`)
}) })
test('if you use the format keyboard binding it formats your code and executes so lints are shown', async ({ test('if you use the format keyboard binding it formats your code and executes so lints are shown', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch_001 = startSketchOn('XY') `sketch_001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)` |> close(%)`
) )
localStorage.setItem('disableAxis', 'true') localStorage.setItem('disableAxis', 'true')
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -348,11 +353,11 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch_001 = startSketchOn('XY') .toHaveText(`sketch_001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)`) |> close(%)`)
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
@ -364,14 +369,11 @@ test.describe('Editor tests', () => {
).toBeVisible() ).toBeVisible()
}) })
test('if you write kcl with lint errors you get lints', async ({ test('if you write kcl with lint errors you get lints', async ({ page }) => {
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible()
@ -407,26 +409,23 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible()
}) })
test('if you fixup kcl errors you clear lints', async ({ test('if you fixup kcl errors you clear lints', async ({ page }) => {
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([3.29, 7.86], %) |> startProfileAt([3.29, 7.86], %)
|> line([2.48, 2.44], %) |> line([2.48, 2.44], %)
|> line([2.66, 1.17], %) |> line([2.66, 1.17], %)
|> close(%) |> close(%)
` `
) )
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -448,23 +447,20 @@ test.describe('Editor tests', () => {
).not.toBeVisible() ).not.toBeVisible()
}) })
test('if you write invalid kcl you get inlined errors', async ({ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
/* add the following code to the editor ($ error is not a valid line) /* add the following code to the editor ($ error is not a valid line)
$ error $ error
const topAng = 30 const topAng = 30
const bottomAng = 25 const bottomAng = 25
*/ */
await u.codeLocator.click() await u.codeLocator.click()
await page.keyboard.type('$ error') await page.keyboard.type('$ error')
@ -522,36 +518,31 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
}) })
test.fixme('error with 2 source ranges gets 2 diagnostics', async ({ test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => {
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`length = .750 `length = .750
width = 0.500 width = 0.500
height = 0.500 height = 0.500
dia = 4 dia = 4
fn squareHole = (l, w) => { fn squareHole = (l, w) => {
squareHoleSketch = startSketchOn('XY') squareHoleSketch = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %) |> startProfileAt([-width / 2, -length / 2], %)
|> lineTo([width / 2, -length / 2], %) |> lineTo([width / 2, -length / 2], %)
|> lineTo([width / 2, length / 2], %) |> lineTo([width / 2, length / 2], %)
|> lineTo([-width / 2, length / 2], %) |> lineTo([-width / 2, length / 2], %)
|> close(%) |> close(%)
return squareHoleSketch return squareHoleSketch
} }
` `
) )
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -577,9 +568,9 @@ test.describe('Editor tests', () => {
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page.keyboard.type(`extrusion = startSketchOn('XY') await page.keyboard.type(`extrusion = startSketchOn('XY')
|> circle({ center: [0, 0], radius: dia/2 }, %) |> circle({ center = [0, 0], radius = dia/2 }, %)
|> hole(squareHole(length, width, height), %) |> hole(squareHole(length, width, height), %)
|> extrude(height, %)`) |> extrude(height, %)`)
// error in gutter // error in gutter
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
@ -592,38 +583,38 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2) await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2)
}) })
test('if your kcl gets an error from the engine it is inlined', async ({ test('if your kcl gets an error from the engine it is inlined', async ({
context,
page, page,
homePage,
}) => { }) => {
await context.addInitScript(async () => { const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`box = startSketchOn('XY') `box = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([0, 10], %) |> line([0, 10], %)
|> line([10, 0], %) |> line([10, 0], %)
|> line([0, -10], %, $revolveAxis) |> line([0, -10], %, $revolveAxis)
|> close(%) |> close(%)
|> extrude(10, %) |> extrude(10, %)
sketch001 = startSketchOn(box, revolveAxis) sketch001 = startSketchOn(box, revolveAxis)
|> startProfileAt([5, 10], %) |> startProfileAt([5, 10], %)
|> line([0, -10], %) |> line([0, -10], %)
|> line([2, 0], %) |> line([2, 0], %)
|> line([0, -10], %) |> line([0, -10], %)
|> close(%) |> close(%)
|> revolve({ |> revolve({
axis: revolveAxis, axis = revolveAxis,
angle: 90 angle = 90
}, %) }, %)
` `
) )
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await page.goto('/')
await u.waitForPageLoad()
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
@ -634,15 +625,12 @@ test.describe('Editor tests', () => {
await expect(page.getByText(searchText)).toBeVisible() await expect(page.getByText(searchText)).toBeVisible()
}) })
test.describe('Autocomplete works', () => { test.describe('Autocomplete works', () => {
test('with enter/click to accept the completion', async ({ test('with enter/click to accept the completion', async ({ page }) => {
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// tests clicking on an option, selection the first option // tests clicking on an option, selection the first option
// and arrowing down to an option // and arrowing down to an option
@ -704,19 +692,19 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([3.14, 12], %) |> startProfileAt([3.14, 12], %)
|> xLine(5, %) // lin`) |> xLine(5, %) // lin`)
// expect there to be no KCL errors // expect there to be no KCL errors
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0) await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0)
}) })
test('with tab to accept the completion', async ({ page, homePage }) => { test('with tab to accept the completion', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
// 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.
@ -778,30 +766,26 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([3.14, 12], %) |> startProfileAt([3.14, 12], %)
|> xLine(5, %) // lin`) |> xLine(5, %) // lin`)
}) })
}) })
test('Can undo a click and point extrude with ctrl+z', async ({ test('Can undo a click and point extrude with ctrl+z', async ({ page }) => {
page,
context,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await context.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %) |> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %) |> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %) |> tangentialArcTo([24.95, -5.38], %)
|> close(%)` |> close(%)`
) )
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -854,32 +838,29 @@ test.describe('Editor tests', () => {
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %) |> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %) |> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %) |> tangentialArcTo([24.95, -5.38], %)
|> close(%)`) |> close(%)`)
}) })
test('Can undo a sketch modification with ctrl+z', async ({ test('Can undo a sketch modification with ctrl+z', async ({ page }) => {
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -10.01], %) |> startProfileAt([4.61, -10.01], %)
|> line([12.73, -0.09], %) |> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close(%) |> close(%)
|> extrude(5, %)` |> extrude(5, %)`
) )
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -906,7 +887,7 @@ test.describe('Editor tests', () => {
}) })
await page.waitForTimeout(100) await page.waitForTimeout(100)
const startPX = [1200 / 2, 500 / 2] const startPX = [665, 397]
const dragPX = 40 const dragPX = 40
@ -920,9 +901,9 @@ test.describe('Editor tests', () => {
await expect(page.getByTestId('segment-overlay')).toHaveCount(2) await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
// drag startProfileAt handle // drag startProfieAt handle
await page.dragAndDrop('#stream', '#stream', { await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0] + 68, y: startPX[1] + 147 }, sourcePosition: { x: startPX[0], y: startPX[1] },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX }, targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
}) })
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -960,12 +941,12 @@ test.describe('Editor tests', () => {
// expect the code to have changed // expect the code to have changed
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([2.71, -2.71], %) |> startProfileAt([7.12, -12.68], %)
|> line([15.4, -2.78], %) |> line([15.39, -2.78], %)
|> tangentialArcTo([27.6, -3.05], %) |> tangentialArcTo([27.6, -3.05], %)
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
`) `)
// Hit undo // Hit undo
await page.keyboard.down('Control') await page.keyboard.down('Control')
@ -974,11 +955,11 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([2.71, -2.71], %) |> startProfileAt([7.12, -12.68], %)
|> line([15.4, -2.78], %) |> line([15.39, -2.78], %)
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close(%) |> close(%)
|> extrude(5, %)`) |> extrude(5, %)`)
// Hit undo again. // Hit undo again.
await page.keyboard.down('Control') await page.keyboard.down('Control')
@ -987,12 +968,12 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([2.71, -2.71], %) |> startProfileAt([7.12, -12.68], %)
|> line([12.73, -0.09], %) |> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
`) `)
// Hit undo again. // Hit undo again.
await page.keyboard.down('Control') await page.keyboard.down('Control')
@ -1002,29 +983,31 @@ test.describe('Editor tests', () => {
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -10.01], %) |> startProfileAt([4.61, -10.01], %)
|> line([12.73, -0.09], %) |> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close(%) |> close(%)
|> extrude(5, %)`) |> extrude(5, %)`)
}) })
test.fixme( test.fixme(
`Can use the import stdlib function on a local OBJ file`, `Can use the import stdlib function on a local OBJ file`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ page, context }, testInfo) => { async ({ browserName }, testInfo) => {
await context.folderSetupFn(async (dir) => { const { electronApp, page } = await setupElectron({
const bracketDir = join(dir, 'cube') testInfo,
await fsp.mkdir(bracketDir, { recursive: true }) folderSetupFn: async (dir) => {
await fsp.copyFile( const bracketDir = join(dir, 'cube')
executorInputPath('cube.obj'), await fsp.mkdir(bracketDir, { recursive: true })
join(bracketDir, 'cube.obj') await fsp.copyFile(
) executorInputPath('cube.obj'),
await fsp.writeFile(join(bracketDir, 'main.kcl'), '') join(bracketDir, 'cube.obj')
)
await fsp.writeFile(join(bracketDir, 'main.kcl'), '')
},
}) })
const viewportSize = { width: 1200, height: 500 } const viewportSize = { width: 1200, height: 500 }
await page.setBodyDimensions(viewportSize) await page.setViewportSize(viewportSize)
// Locators and constants // Locators and constants
const u = await getUtils(page) const u = await getUtils(page)
@ -1082,6 +1065,8 @@ test.describe('Editor tests', () => {
}) })
.toBeGreaterThan(15) .toBeGreaterThan(15)
}) })
await electronApp.close()
} }
) )
}) })

File diff suppressed because it is too large Load Diff

View File

@ -54,13 +54,13 @@ export class EditorFixture {
} }
} }
if (!shouldNormalise) { if (!shouldNormalise) {
const expectStart = expect.poll(() => this.codeContent.textContent()) const expectStart = expect(this.codeContent)
if (not) { if (not) {
const result = await expectStart.not.toContain(code) const result = await expectStart.not.toContainText(code, { timeout })
await resetPane() await resetPane()
return result return result
} }
const result = await expectStart.toContain(code) const result = await expectStart.toContainText(code, { timeout })
await resetPane() await resetPane()
return result return result
} }
@ -147,20 +147,4 @@ export class EditorFixture {
openPane() { openPane() {
return openPane(this.page, this.paneButtonTestId) return openPane(this.page, this.paneButtonTestId)
} }
scrollToText(text: string) {
return this.page.evaluate((scrollToText: string) => {
// editorManager is available on the window object.
// @ts-ignore
let index = editorManager._editorView.docView.view.state.doc
.toString()
.indexOf(scrollToText)
// @ts-ignore
editorManager._editorView.dispatch({
selection: {
anchor: index,
},
scrollIntoView: true,
})
}, text)
}
} }

View File

@ -1,11 +1,11 @@
import type { import type {
BrowserContext, BrowserContext,
ElectronApplication, ElectronApplication,
TestInfo,
Page, Page,
TestInfo,
} from '@playwright/test' } from '@playwright/test'
import { test as base } from '@playwright/test'
import { getUtils, setup, setupElectron } from '../test-utils' import { getUtils, setup, setupElectron, tearDown } from '../test-utils'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { join } from 'path' import { join } from 'path'
import { CmdBarFixture } from './cmdBarFixture' import { CmdBarFixture } from './cmdBarFixture'
@ -20,11 +20,11 @@ export class AuthenticatedApp {
public readonly page: Page public readonly page: Page
public readonly context: BrowserContext public readonly context: BrowserContext
public readonly testInfo: TestInfo public readonly testInfo: TestInfo
public readonly viewPortSize = { width: 1200, height: 500 } public readonly viewPortSize = { width: 1000, height: 500 }
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) { constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this.context = context
this.page = page this.page = page
this.context = context
this.testInfo = testInfo this.testInfo = testInfo
} }
@ -49,7 +49,9 @@ export class AuthenticatedApp {
} }
} }
export interface Fixtures { interface Fixtures {
app: AuthenticatedApp
tronApp: AuthenticatedTronApp
cmdBar: CmdBarFixture cmdBar: CmdBarFixture
editor: EditorFixture editor: EditorFixture
toolbar: ToolbarFixture toolbar: ToolbarFixture
@ -59,11 +61,9 @@ export interface Fixtures {
export class AuthenticatedTronApp { export class AuthenticatedTronApp {
public readonly _page: Page public readonly _page: Page
public page: Page public page: Page
public context: BrowserContext public readonly context: BrowserContext
public readonly testInfo: TestInfo public readonly testInfo: TestInfo
public electronApp?: ElectronApplication public electronApp?: ElectronApplication
public readonly viewPortSize = { width: 1200, height: 500 }
public dir: string = ''
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) { constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this._page = page this._page = page
@ -79,24 +79,15 @@ export class AuthenticatedTronApp {
appSettings?: Partial<SaveSettingsPayload> appSettings?: Partial<SaveSettingsPayload>
} = { fixtures: {} } } = { fixtures: {} }
) { ) {
const { electronApp, page, context, dir } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo: this.testInfo, testInfo: this.testInfo,
folderSetupFn: arg.folderSetupFn, folderSetupFn: arg.folderSetupFn,
cleanProjectDir: arg.cleanProjectDir, cleanProjectDir: arg.cleanProjectDir,
appSettings: arg.appSettings, appSettings: arg.appSettings,
}) })
this.page = page this.page = page
this.context = context
this.electronApp = electronApp this.electronApp = electronApp
this.dir = dir await page.setViewportSize({ width: 1200, height: 500 })
// Easier to access throughout utils
this.page.dir = dir
// Setup localStorage, addCookies, reload
await setup(this.context, this.page, this.testInfo)
await page.setViewportSize(this.viewPortSize)
for (const key of unsafeTypedKeys(arg.fixtures)) { for (const key of unsafeTypedKeys(arg.fixtures)) {
const fixture = arg.fixtures[key] const fixture = arg.fixtures[key]
@ -119,20 +110,32 @@ export class AuthenticatedTronApp {
}) })
} }
export const fixtures = { export const test = base.extend<Fixtures>({
cmdBar: async ({ page }: { page: Page }, use: any) => { app: async ({ page, context }, use, testInfo) => {
await use(new AuthenticatedApp(context, page, testInfo))
},
tronApp: async ({ page, context }, use, testInfo) => {
await use(new AuthenticatedTronApp(context, page, testInfo))
},
cmdBar: async ({ page }, use) => {
await use(new CmdBarFixture(page)) await use(new CmdBarFixture(page))
}, },
editor: async ({ page }: { page: Page }, use: any) => { editor: async ({ page }, use) => {
await use(new EditorFixture(page)) await use(new EditorFixture(page))
}, },
toolbar: async ({ page }: { page: Page }, use: any) => { toolbar: async ({ page }, use) => {
await use(new ToolbarFixture(page)) await use(new ToolbarFixture(page))
}, },
scene: async ({ page }: { page: Page }, use: any) => { scene: async ({ page }, use) => {
await use(new SceneFixture(page)) await use(new SceneFixture(page))
}, },
homePage: async ({ page }: { page: Page }, use: any) => { homePage: async ({ page }, use) => {
await use(new HomePageFixture(page)) await use(new HomePageFixture(page))
}, },
} })
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
export { expect } from '@playwright/test'

View File

@ -4,6 +4,7 @@ import { expect } from '@playwright/test'
interface ProjectCardState { interface ProjectCardState {
title: string title: string
fileCount: number fileCount: number
folderCount: number
} }
interface HomePageState { interface HomePageState {
@ -14,14 +15,10 @@ interface HomePageState {
export class HomePageFixture { export class HomePageFixture {
public page: Page public page: Page
projectSection!: Locator
projectCard!: Locator projectCard!: Locator
projectCardTitle!: Locator projectCardTitle!: Locator
projectCardFile!: Locator projectCardFile!: Locator
projectCardFolder!: Locator projectCardFolder!: Locator
projectButtonNew!: Locator
projectButtonContinue!: Locator
projectTextName!: Locator
sortByDateBtn!: Locator sortByDateBtn!: Locator
sortByNameBtn!: Locator sortByNameBtn!: Locator
@ -32,19 +29,11 @@ export class HomePageFixture {
reConstruct = (page: Page) => { reConstruct = (page: Page) => {
this.page = page this.page = page
this.projectSection = this.page.getByTestId('home-section')
this.projectCard = this.page.getByTestId('project-link') this.projectCard = this.page.getByTestId('project-link')
this.projectCardTitle = this.page.getByTestId('project-title') this.projectCardTitle = this.page.getByTestId('project-title')
this.projectCardFile = this.page.getByTestId('project-file-count') this.projectCardFile = this.page.getByTestId('project-file-count')
this.projectCardFolder = this.page.getByTestId('project-folder-count') this.projectCardFolder = this.page.getByTestId('project-folder-count')
this.projectButtonNew = this.page.getByTestId('home-new-file')
this.projectTextName = this.page.getByTestId('cmd-bar-arg-value')
this.projectButtonContinue = this.page.getByRole('button', {
name: 'Continue',
})
this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified') this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified')
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name') this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
} }
@ -72,13 +61,15 @@ export class HomePageFixture {
const projectCards = await this.projectCard.all() const projectCards = await this.projectCard.all()
const projectCardStates: Array<ProjectCardState> = [] const projectCardStates: Array<ProjectCardState> = []
for (const projectCard of projectCards) { for (const projectCard of projectCards) {
const [title, fileCount] = await Promise.all([ const [title, fileCount, folderCount] = await Promise.all([
(await projectCard.locator(this.projectCardTitle).textContent()) || '', (await projectCard.locator(this.projectCardTitle).textContent()) || '',
Number(await projectCard.locator(this.projectCardFile).textContent()), Number(await projectCard.locator(this.projectCardFile).textContent()),
Number(await projectCard.locator(this.projectCardFolder).textContent()),
]) ])
projectCardStates.push({ projectCardStates.push({
title: title, title: title,
fileCount, fileCount,
folderCount,
}) })
} }
return projectCardStates return projectCardStates
@ -103,25 +94,10 @@ export class HomePageFixture {
.toEqual(expectedState) .toEqual(expectedState)
} }
createAndGoToProject = async (projectTitle: string) => {
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
await this.projectButtonNew.click()
await this.projectTextName.click()
await this.projectTextName.fill(projectTitle)
await this.projectButtonContinue.click()
}
openProject = async (projectTitle: string) => { openProject = async (projectTitle: string) => {
const projectCard = this.projectCard.locator( const projectCard = this.projectCard.locator(
this.page.getByText(projectTitle) this.page.getByText(projectTitle)
) )
await projectCard.click() await projectCard.click()
} }
goToModelingScene = async (name: string = 'testDefault') => {
// On web this is a no-op. There is no project view.
if (process.env.PLATFORM === 'web') return
await this.createAndGoToProject(name)
}
} }

View File

@ -28,7 +28,6 @@ type SceneSerialised = {
type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean> type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean> type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean>
type DblClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean> type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean>
type DragFromHandler = ( type DragFromHandler = (
dragParams: mouseDragFromParams dragParams: mouseDragFromParams
@ -53,9 +52,8 @@ export class SceneFixture {
expectState = async (expected: SceneSerialised) => { expectState = async (expected: SceneSerialised) => {
return expect return expect
.poll(async () => await this._serialiseScene(), { .poll(() => this._serialiseScene(), {
intervals: [1_000, 2_000, 10_000], message: `Expected scene state to match`,
timeout: 60000,
}) })
.toEqual(expected) .toEqual(expected)
} }
@ -70,7 +68,7 @@ export class SceneFixture {
x: number, x: number,
y: number, y: number,
{ steps }: { steps: number } = { steps: 20 } { steps }: { steps: number } = { steps: 20 }
): [ClickHandler, MoveHandler, DblClickHandler] => ): [ClickHandler, MoveHandler] =>
[ [
(clickParams?: mouseParams) => { (clickParams?: mouseParams) => {
if (clickParams?.pixelDiff) { if (clickParams?.pixelDiff) {
@ -92,16 +90,6 @@ export class SceneFixture {
} }
return this.page.mouse.move(x, y, { steps }) return this.page.mouse.move(x, y, { steps })
}, },
(clickParams?: mouseParams) => {
if (clickParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
() => this.page.mouse.dblclick(x, y),
clickParams.pixelDiff
)
}
return this.page.mouse.dblclick(x, y)
},
] as const ] as const
makeDragHelpers = ( makeDragHelpers = (
x: number, x: number,
@ -188,10 +176,7 @@ export class SceneFixture {
type: 'default_camera_get_settings', type: 'default_camera_get_settings',
}, },
}) })
await this.page await this.waitForExecutionDone()
.locator(`[data-receive-command-type="default_camera_get_settings"]`)
.first()
.waitFor()
const position = await Promise.all([ const position = await Promise.all([
this.page.getByTestId('cam-x-position').inputValue().then(Number), this.page.getByTestId('cam-x-position').inputValue().then(Number),
this.page.getByTestId('cam-y-position').inputValue().then(Number), this.page.getByTestId('cam-y-position').inputValue().then(Number),
@ -242,7 +227,6 @@ export class SceneFixture {
} }
async clickGizmoMenuItem(name: string) { async clickGizmoMenuItem(name: string) {
await this.gizmo.hover()
await this.gizmo.click({ button: 'right' }) await this.gizmo.click({ button: 'right' })
const buttonToTest = this.page.getByRole('button', { const buttonToTest = this.page.getByRole('button', {
name: name, name: name,

View File

@ -1,12 +1,11 @@
import type { Page, Locator } from '@playwright/test' import type { Page, Locator } from '@playwright/test'
import { expect } from '../zoo-test' import { expect } from './fixtureSetup'
import { doAndWaitForImageDiff } from '../test-utils' import { doAndWaitForImageDiff } from '../test-utils'
export class ToolbarFixture { export class ToolbarFixture {
public page: Page public page: Page
extrudeButton!: Locator extrudeButton!: Locator
loftButton!: Locator
offsetPlaneButton!: Locator offsetPlaneButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
lineBtn!: Locator lineBtn!: Locator
@ -27,7 +26,6 @@ export class ToolbarFixture {
reConstruct = (page: Page) => { reConstruct = (page: Page) => {
this.page = page this.page = page
this.extrudeButton = page.getByTestId('extrude') this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.offsetPlaneButton = page.getByTestId('plane-offset') this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch') this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line') this.lineBtn = page.getByTestId('line')

View File

@ -1,22 +1,29 @@
import { test, expect } from './zoo-test' import { test, expect } from '@playwright/test'
import { executorInputPath } from './test-utils' import { setupElectron, tearDown, executorInputPath } from './test-utils'
import { join } from 'path' import { join } from 'path'
import fsp from 'fs/promises' import fsp from 'fs/promises'
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test( test(
'When machine-api server not found butt is disabled and shows the reason', 'When machine-api server not found butt is disabled and shows the reason',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ browserName }, testInfo) => {
await context.folderSetupFn(async (dir) => { const { electronApp, page } = await setupElectron({
const bracketDir = join(dir, 'bracket') testInfo,
await fsp.mkdir(bracketDir, { recursive: true }) folderSetupFn: async (dir) => {
await fsp.copyFile( const bracketDir = join(dir, 'bracket')
executorInputPath('focusrite_scarlett_mounting_braket.kcl'), await fsp.mkdir(bracketDir, { recursive: true })
join(bracketDir, 'main.kcl') await fsp.copyFile(
) executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
},
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
@ -40,23 +47,28 @@ test(
// that the machine-api server is not found // that the machine-api server is not found
await makeButton.hover() await makeButton.hover()
await expect(page.getByText(notFoundText).first()).toBeVisible() await expect(page.getByText(notFoundText).first()).toBeVisible()
await electronApp.close()
} }
) )
test( test(
'When machine-api server not found home screen & project status shows the reason', 'When machine-api server not found home screen & project status shows the reason',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ browserName }, testInfo) => {
await context.folderSetupFn(async (dir) => { const { electronApp, page } = await setupElectron({
const bracketDir = join(dir, 'bracket') testInfo,
await fsp.mkdir(bracketDir, { recursive: true }) folderSetupFn: async (dir) => {
await fsp.copyFile( const bracketDir = join(dir, 'bracket')
executorInputPath('focusrite_scarlett_mounting_braket.kcl'), await fsp.mkdir(bracketDir, { recursive: true })
join(bracketDir, 'main.kcl') await fsp.copyFile(
) executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
},
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const notFoundText = 'Machine API server was not discovered' const notFoundText = 'Machine API server was not discovered'
@ -79,5 +91,7 @@ test(
await networkMachineToggle.hover() await networkMachineToggle.hover()
await expect(page.getByText(notFoundText).nth(1)).toBeVisible() await expect(page.getByText(notFoundText).nth(1)).toBeVisible()
await electronApp.close()
} }
) )

View File

@ -1,12 +0,0 @@
// These tests are meant to simply test starting and stopping the electron
// application, check it can make it to the project pane, and nothing more.
// It also tests our test wrappers are working.
// Additionally this serves as a nice minimal example.
import { test, expect } from './zoo-test'
test.describe('Open the application', () => {
test('see the project view', async ({ page, context }) => {
await expect(page.getByTestId('home-section')).toBeVisible()
})
})

View File

@ -1,63 +1,79 @@
import { test, expect } from './zoo-test' import { test, expect } from '@playwright/test'
import { join } from 'path' import { join } from 'path'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { getUtils, executorInputPath, createProject } from './test-utils' import {
getUtils,
setup,
setupElectron,
tearDown,
executorInputPath,
createProject,
} from './test-utils'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { import {
TEST_SETTINGS_KEY, TEST_SETTINGS_KEY,
TEST_SETTINGS_ONBOARDING_START, TEST_SETTINGS_ONBOARDING_START,
TEST_SETTINGS_ONBOARDING_EXPORT, TEST_SETTINGS_ONBOARDING_EXPORT,
TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING,
TEST_SETTINGS_ONBOARDING_USER_MENU, TEST_SETTINGS_ONBOARDING_USER_MENU,
} from './storageStates' } from './storageStates'
import * as TOML from '@iarna/toml' import * as TOML from '@iarna/toml'
// Because onboarding relies on an app setting we need to set it as incompletel test.beforeEach(async ({ context, page }, testInfo) => {
// for all these tests. if (testInfo.tags.includes('@electron')) {
return
}
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Onboarding tests', () => { test.describe('Onboarding tests', () => {
test( test('Onboarding code is shown in the editor', async ({ page }) => {
'Onboarding code is shown in the editor', const u = await getUtils(page)
{
appSettings: { // Override beforeEach test setup
app: { await page.addInitScript(
onboardingStatus: 'incomplete', async ({ settingsKey }) => {
}, // Give no initial code, so that the onboarding start is shown immediately
localStorage.removeItem('persistCode')
localStorage.removeItem(settingsKey)
}, },
cleanProjectDir: true, { settingsKey: TEST_SETTINGS_KEY }
}, )
async ({ context, page, homePage }) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded await page.setViewportSize({ width: 1200, height: 500 })
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// *and* that the code is shown in the editor await u.waitForAuthSkipAppStart()
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket' // Test that the onboarding pane loaded
) await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
}
) // *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
})
test( test(
'Desktop: fresh onboarding executes and loads', 'Desktop: fresh onboarding executes and loads',
{ { tag: '@electron' },
tag: '@electron', async ({ browserName: _ }, testInfo) => {
appSettings: { const { electronApp, page } = await setupElectron({
app: { testInfo,
onboardingStatus: 'incomplete', appSettings: {
app: {
onboardingStatus: 'incomplete',
},
}, },
}, cleanProjectDir: true,
cleanProjectDir: true, })
},
async ({ page, homePage }, testInfo) => {
const u = await getUtils(page) const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 } const viewportSize = { width: 1200, height: 500 }
await page.setBodyDimensions(viewportSize) await page.setViewportSize(viewportSize)
await test.step(`Create a project and open to the onboarding`, async () => { await test.step(`Create a project and open to the onboarding`, async () => {
await createProject({ name: 'project-link', page }) await createProject({ name: 'project-link', page })
@ -77,361 +93,320 @@ test.describe('Onboarding tests', () => {
'// Shelf Bracket' '// Shelf Bracket'
) )
}) })
await electronApp.close()
} }
) )
test( test('Code resets after confirmation', async ({ page }) => {
'Code resets after confirmation', const initialCode = `sketch001 = startSketchOn('XZ')`
{
appSettings: {
app: {
onboardingStatus: 'incomplete',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
const initialCode = `sketch001 = startSketchOn('XZ')`
// Load the page up with some code so we see the confirmation warning // Load the page up with some code so we see the confirmation warning
// when we go to replay onboarding // when we go to replay onboarding
await context.addInitScript((code) => { await page.addInitScript((code) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
}, initialCode) }, initialCode)
await page.setBodyDimensions({ width: 1200, height: 500 }) const u = await getUtils(page)
await homePage.goToModelingScene() await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// Replay the onboarding // Replay the onboarding
await page.getByRole('link', { name: 'Settings' }).last().click() await page.getByRole('link', { name: 'Settings' }).last().click()
const replayButton = page.getByRole('button', { const replayButton = page.getByRole('button', { name: 'Replay onboarding' })
name: 'Replay onboarding', await expect(replayButton).toBeVisible()
await replayButton.click()
// Ensure we see the warning, and that the code has not yet updated
await expect(
page.getByText('Replaying onboarding resets your code')
).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(initialCode)
const nextButton = page.getByTestId('onboarding-next')
await expect(nextButton).toBeVisible()
await nextButton.click()
// Ensure we see the introduction and that the code has been reset
await expect(page.getByText('Welcome to Modeling App!')).toBeVisible()
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// Ensure we persisted the code to local storage.
// Playwright's addInitScript method unfortunately will reset
// this code if we try reloading the page as a test,
// so this is our best way to test persistence afaik.
expect(
await page.evaluate(() => {
return localStorage.getItem('persistCode')
}) })
await expect(replayButton).toBeVisible() ).toContain('// Shelf Bracket')
await replayButton.click() })
// Ensure we see the warning, and that the code has not yet updated test('Click through each onboarding step', async ({ page }) => {
await expect(page.getByText('Would you like to create')).toBeVisible() const u = await getUtils(page)
await expect(page.locator('.cm-content')).toHaveText(initialCode)
const nextButton = page.getByTestId('onboarding-next') // Override beforeEach test setup
await nextButton.hover() await page.addInitScript(
await nextButton.click() async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately
// Ensure we see the introduction and that the code has been reset localStorage.setItem('persistCode', '')
await expect(page.getByText('Welcome to Modeling App!')).toBeVisible() localStorage.setItem(settingsKey, settings)
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
// There used to be old code here that checked if we stored the reset
// code into localStorage but that isnt the case on desktop. It gets
// saved to the file system, which we have other tests for.
}
)
test(
'Click through each onboarding step',
{
appSettings: {
app: {
onboardingStatus: 'incomplete',
},
}, },
}, {
async ({ context, page, homePage }) => { settingsKey: TEST_SETTINGS_KEY,
// Override beforeEach test setup settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_START }),
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.setItem('persistCode', '')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({
settings: TEST_SETTINGS_ONBOARDING_START,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover()
await nextButton.click()
} }
)
// Finish the onboarding await page.setViewportSize({ width: 1200, height: 1080 })
await nextButton.hover()
await u.waitForAuthSkipAppStart()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
while ((await nextButton.innerText()) !== 'Finish') {
await expect(nextButton).toBeVisible()
await nextButton.click() await nextButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect.poll(() => page.url()).not.toContain('/onboarding')
} }
)
test( // Finish the onboarding
'Onboarding redirects and code updating', await expect(nextButton).toBeVisible()
{ await nextButton.click()
appSettings: {
app: { // Test that the onboarding pane is gone
onboardingStatus: '/export', await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
}, await expect(page.url()).not.toContain('onboarding')
})
test('Onboarding redirects and code updating', async ({ page }) => {
const u = await getUtils(page)
// Override beforeEach test setup
await page.addInitScript(
async ({ settingsKey, settings }) => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', 'sigmaAllow = 15000')
localStorage.setItem(settingsKey, settings)
}, },
cleanProjectDir: true, {
}, settingsKey: TEST_SETTINGS_KEY,
async ({ context, page, homePage }) => { settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_EXPORT }),
const originalCode = 'sigmaAllow = 15000'
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', originalCode)
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({
settings: TEST_SETTINGS_ONBOARDING_EXPORT,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the redirect happened
await expect.poll(() => page.url()).toContain('/onboarding/export')
// Test that you come back to this page when you refresh
await page.reload()
await expect.poll(() => page.url()).toContain('/onboarding/export')
// Test that the code changes when you advance to the next step
await page.getByTestId('onboarding-next').hover()
await page.getByTestId('onboarding-next').click()
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
await expect(page.locator('.cm-content')).not.toHaveText(originalCode)
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
}
)
test(
'Onboarding code gets reset to demo on Interactive Numbers step',
{
appSettings: {
app: {
onboardingStatus: '/parametric-modeling',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
await expect
.poll(() => page.url())
.toContain(onboardingPaths.PARAMETRIC_MODELING)
const bracketNoNewLines = bracket.replace(/\n/g, '')
// Check the code got reset on load
await expect(page.locator('#code-pane')).toBeVisible()
await expect(u.codeLocator).toHaveText(bracketNoNewLines, {
timeout: 10_000,
})
// Mess with the code again
await u.codeLocator.selectText()
await u.codeLocator.fill(badCode)
await expect(u.codeLocator).toHaveText(badCode)
// Click to the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, {
waitUntil: 'domcontentloaded',
})
// Check that the code has been reset
await expect(u.codeLocator).toHaveText(bracketNoNewLines)
}
)
// (lee) The two avatar tests are weird because even on main, we don't have
// anything to do with the avatar inside the onboarding test. Due to the
// low impact of an avatar not showing I'm changing this to fixme.
test.fixme(
'Avatar text updates depending on image load success',
{
appSettings: {
app: {
onboardingStatus: 'incomplete',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({
settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the text in this step is correct
const avatarLocator = await page
.getByTestId('user-sidebar-toggle')
.locator('img')
const onboardingOverlayLocator = await page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
// Expect the avatar to be visible and for the text to reference it
await expect(avatarLocator).toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('your avatar')
// This is to force the avatar to 404.
// For our test image (only triggers locally. on CI, it's Kurt's /
// gravatar image )
await page.route('/cat.jpg', async (route) => {
await route.fulfill({
status: 404,
contentType: 'text/plain',
body: 'Not Found!',
})
})
// 404 the CI avatar image
await page.route(
'https://lh3.googleusercontent.com/**',
async (route) => {
await route.fulfill({
status: 404,
contentType: 'text/plain',
body: 'Not Found!',
})
}
)
await page.reload({ waitUntil: 'domcontentloaded' })
// Now expect the text to be different
await expect(avatarLocator).not.toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('the menu button')
}
)
test.fixme(
"Avatar text doesn't mention avatar when no avatar",
{
appSettings: {
app: {
onboardingStatus: 'incomplete',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
localStorage.setItem('FORCE_NO_IMAGE', 'FORCE_NO_IMAGE')
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({
settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the text in this step is correct
const sidebar = page.getByTestId('user-sidebar-toggle')
const avatar = sidebar.locator('img')
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
// Expect the avatar to be visible and for the text to reference it
await expect(avatar).not.toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('the menu button')
// Test we mention what else is in this menu for https://github.com/KittyCAD/modeling-app/issues/2939
// which doesn't deserver its own full test spun up
const userMenuFeatures = [
'manage your account',
'report a bug',
'request a feature',
'sign out',
]
for (const feature of userMenuFeatures) {
await expect(onboardingOverlayLocator).toContainText(feature)
} }
)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// Test that the redirect happened
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
)
// Test that you come back to this page when you refresh
await page.reload()
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
)
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
// Test that the code changes when you advance to the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText('')
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
})
test('Onboarding code gets reset to demo on Interactive Numbers step', async ({
page,
}) => {
test.skip(
process.platform === 'darwin',
"Skip on macOS, because Playwright isn't behaving the same as the actual browser"
)
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
// Override beforeEach test setup
await page.addInitScript(
async ({ settingsKey, settings, badCode }) => {
localStorage.setItem('persistCode', badCode)
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({
settings: TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING,
}),
badCode,
}
)
await page.setViewportSize({ width: 1200, height: 1080 })
await u.waitForAuthSkipAppStart()
await page.waitForURL('**' + onboardingPaths.PARAMETRIC_MODELING, {
waitUntil: 'domcontentloaded',
})
const bracketNoNewLines = bracket.replace(/\n/g, '')
// Check the code got reset on load
await expect(page.locator('#code-pane')).toBeVisible()
await expect(u.codeLocator).toHaveText(bracketNoNewLines, {
timeout: 10_000,
})
// Mess with the code again
await u.codeLocator.selectText()
await u.codeLocator.fill(badCode)
await expect(u.codeLocator).toHaveText(badCode)
// Click to the next step
await page.locator('[data-testid="onboarding-next"]').click()
await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, {
waitUntil: 'domcontentloaded',
})
// Check that the code has been reset
await expect(u.codeLocator).toHaveText(bracketNoNewLines)
})
test('Avatar text updates depending on image load success', async ({
page,
}) => {
// Override beforeEach test setup
await page.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({
settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
}),
}
)
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
// Test that the text in this step is correct
const avatarLocator = await page
.getByTestId('user-sidebar-toggle')
.locator('img')
const onboardingOverlayLocator = await page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
// Expect the avatar to be visible and for the text to reference it
await expect(avatarLocator).toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('your avatar')
// This is to force the avatar to 404.
// For our test image (only triggers locally. on CI, it's Kurt's /
// gravatar image )
await page.route('/cat.jpg', async (route) => {
await route.fulfill({
status: 404,
contentType: 'text/plain',
body: 'Not Found!',
})
})
// 404 the CI avatar image
await page.route('https://lh3.googleusercontent.com/**', async (route) => {
await route.fulfill({
status: 404,
contentType: 'text/plain',
body: 'Not Found!',
})
})
await page.reload({ waitUntil: 'domcontentloaded' })
// Now expect the text to be different
await expect(avatarLocator).not.toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('the menu button')
})
test("Avatar text doesn't mention avatar when no avatar", async ({
page,
}) => {
// Override beforeEach test setup
await page.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
localStorage.setItem('FORCE_NO_IMAGE', 'FORCE_NO_IMAGE')
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({
settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
}),
}
)
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
// Test that the text in this step is correct
const sidebar = page.getByTestId('user-sidebar-toggle')
const avatar = sidebar.locator('img')
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
// Expect the avatar to be visible and for the text to reference it
await expect(avatar).not.toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('the menu button')
// Test we mention what else is in this menu for https://github.com/KittyCAD/modeling-app/issues/2939
// which doesn't deserver its own full test spun up
const userMenuFeatures = [
'manage your account',
'report a bug',
'request a feature',
'sign out',
]
for (const feature of userMenuFeatures) {
await expect(onboardingOverlayLocator).toContainText(feature)
} }
) })
}) })
test( test(
'Restarting onboarding on desktop takes one attempt', 'Restarting onboarding on desktop takes one attempt',
{ { tag: '@electron' },
appSettings: { async ({ browser: _ }, testInfo) => {
app: { const { electronApp, page } = await setupElectron({
onboardingStatus: 'dismissed', testInfo,
folderSetupFn: async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true })
await fsp.copyFile(
executorInputPath('router-template-slate.kcl'),
join(routerTemplateDir, 'main.kcl')
)
}, },
},
cleanProjectDir: true,
},
async ({ context, page, homePage }, testInfo) => {
await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true })
await fsp.copyFile(
executorInputPath('router-template-slate.kcl'),
join(routerTemplateDir, 'main.kcl')
)
}) })
// Our constants // Our constants
@ -443,8 +418,9 @@ test(
const restartOnboardingButton = page.getByRole('button', { const restartOnboardingButton = page.getByRole('button', {
name: 'Reset onboarding', name: 'Reset onboarding',
}) })
const nextButton = page.getByTestId('onboarding-next') const restartConfirmationButton = page.getByRole('button', {
name: 'Make a new project',
})
const tutorialProjectIndicator = page const tutorialProjectIndicator = page
.getByTestId('project-sidebar-toggle') .getByTestId('project-sidebar-toggle')
.filter({ hasText: 'Tutorial Project 00' }) .filter({ hasText: 'Tutorial Project 00' })
@ -463,7 +439,7 @@ test(
}) })
await test.step('Navigate into project', async () => { await test.step('Navigate into project', async () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -479,8 +455,8 @@ test(
await helpMenuButton.click() await helpMenuButton.click()
await restartOnboardingButton.click() await restartOnboardingButton.click()
await nextButton.hover() await expect(restartConfirmationButton).toBeVisible()
await nextButton.click() await restartConfirmationButton.click()
}) })
await test.step('Confirm that the onboarding has restarted', async () => { await test.step('Confirm that the onboarding has restarted', async () => {
@ -504,9 +480,11 @@ test(
await restartOnboardingSettingsButton.click() await restartOnboardingSettingsButton.click()
// Since the code is empty, we should not see the confirmation dialog // Since the code is empty, we should not see the confirmation dialog
await expect(nextButton).not.toBeVisible() await expect(restartConfirmationButton).not.toBeVisible()
await expect(tutorialProjectIndicator).toBeVisible() await expect(tutorialProjectIndicator).toBeVisible()
await expect(tutorialModalText).toBeVisible() await expect(tutorialModalText).toBeVisible()
}) })
await electronApp.close()
} }
) )

View File

@ -1,49 +1,25 @@
import { test, expect, Page } from './zoo-test' import { test, expect, AuthenticatedApp } from './fixtures/fixtureSetup'
import { EditorFixture } from './fixtures/editorFixture' import { EditorFixture } from './fixtures/editorFixture'
import { SceneFixture } from './fixtures/sceneFixture' import { SceneFixture } from './fixtures/sceneFixture'
import { ToolbarFixture } from './fixtures/toolbarFixture' import { ToolbarFixture } from './fixtures/toolbarFixture'
import fs from 'node:fs/promises'
import path from 'node:path'
import { getUtils } from './test-utils'
// test file is for testing point an click code gen functionality that's not sketch mode related // test file is for testing point an click code gen functionality that's not sketch mode related
test('verify extruding circle works', async ({ test(
context, 'verify extruding circle works',
homePage, { tag: ['@skipWin'] },
cmdBar, async ({ app, cmdBar, editor, toolbar, scene }) => {
editor, test.skip(
toolbar, process.platform === 'win32',
scene, 'Fails on windows in CI, can not be replicated locally on windows.'
}) => { )
const file = await fs.readFile( const file = await app.getInputFile('test-circle-extrude.kcl')
path.resolve( await app.initialise(file)
__dirname, const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217)
'../../',
'./src/wasm-lib/tests/executor/inputs/test-circle-extrude.kcl'
),
'utf-8'
)
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217) await test.step('because there is sweepable geometry, verify extrude is enable when nothing is selected', async () => {
await scene.clickNoWhere()
await test.step('because there is sweepable geometry, verify extrude is enable when nothing is selected', async () => { await expect(toolbar.extrudeButton).toBeEnabled()
await scene.clickNoWhere()
await expect(toolbar.extrudeButton).toBeEnabled()
})
await test.step('check code model connection works and that button is still enable once circle is selected ', async () => {
await moveToCircle()
const circleSnippet =
'circle({ center = [318.33, 168.1], radius = 182.8 }, %)'
await editor.expectState({
activeLines: ["constsketch002=startSketchOn('XZ')"],
highlightedCode: circleSnippet,
diagnostics: [],
}) })
await test.step('check code model connection works and that button is still enable once circle is selected ', async () => { await test.step('check code model connection works and that button is still enable once circle is selected ', async () => {
@ -51,7 +27,7 @@ test('verify extruding circle works', async ({
const circleSnippet = const circleSnippet =
'circle({ center = [318.33, 168.1], radius = 182.8 }, %)' 'circle({ center = [318.33, 168.1], radius = 182.8 }, %)'
await editor.expectState({ await editor.expectState({
activeLines: ["constsketch002=startSketchOn('XZ')"], activeLines: [],
highlightedCode: circleSnippet, highlightedCode: circleSnippet,
diagnostics: [], diagnostics: [],
}) })
@ -64,40 +40,39 @@ test('verify extruding circle works', async ({
}) })
await expect(toolbar.extrudeButton).toBeEnabled() await expect(toolbar.extrudeButton).toBeEnabled()
}) })
await expect(toolbar.extrudeButton).toBeEnabled()
})
await test.step('do extrude flow and check extrude code is added to editor', async () => { await test.step('do extrude flow and check extrude code is added to editor', async () => {
await toolbar.extrudeButton.click() await toolbar.extrudeButton.click()
await cmdBar.expectState({ await cmdBar.expectState({
stage: 'arguments', stage: 'arguments',
currentArgKey: 'distance', currentArgKey: 'distance',
currentArgValue: '5', currentArgValue: '5',
headerArguments: { Selection: '1 face', Distance: '' }, headerArguments: { Selection: '1 face', Distance: '' },
highlightedHeaderArg: 'distance', highlightedHeaderArg: 'distance',
commandName: 'Extrude', commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
const expectString = 'extrude001 = extrude(5, sketch001)'
await editor.expectEditor.not.toContain(expectString)
await cmdBar.expectState({
stage: 'review',
headerArguments: { Selection: '1 face', Distance: '5' },
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain(expectString)
}) })
await cmdBar.progressCmdBar() }
)
const expectString = 'extrude001 = extrude(5, sketch001)'
await editor.expectEditor.not.toContain(expectString)
await cmdBar.expectState({
stage: 'review',
headerArguments: { Selection: '1 face', Distance: '5' },
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain(expectString)
})
})
test.describe('verify sketch on chamfer works', () => { test.describe('verify sketch on chamfer works', () => {
const _sketchOnAChamfer = const _sketchOnAChamfer =
( (
page: Page, app: AuthenticatedApp,
editor: EditorFixture, editor: EditorFixture,
toolbar: ToolbarFixture, toolbar: ToolbarFixture,
scene: SceneFixture scene: SceneFixture
@ -149,7 +124,7 @@ test.describe('verify sketch on chamfer works', () => {
await toolbar.startSketchPlaneSelection() await toolbar.startSketchPlaneSelection()
await clickChamfer() await clickChamfer()
// timeout wait for engine animation is unavoidable // timeout wait for engine animation is unavoidable
await page.waitForTimeout(1000) await app.page.waitForTimeout(600)
await editor.expectEditor.toContain(afterChamferSelectSnippet) await editor.expectEditor.toContain(afterChamferSelectSnippet)
}) })
await test.step('make sure a basic sketch can be added', async () => { await test.step('make sure a basic sketch can be added', async () => {
@ -160,9 +135,7 @@ test.describe('verify sketch on chamfer works', () => {
pixelDiff: 50, pixelDiff: 50,
}) })
await rectangle2ndClick() await rectangle2ndClick()
await editor.expectEditor.toContain(afterRectangle2ndClickSnippet, { await editor.expectEditor.toContain(afterRectangle2ndClickSnippet)
shouldNormalise: true,
})
}) })
await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => { await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
@ -177,35 +150,24 @@ test.describe('verify sketch on chamfer works', () => {
}) })
}) })
} }
test('works on all edge selections and can break up multi edges in a chamfer array', async ({ test(
context, 'works on all edge selections and can break up multi edges in a chamfer array',
page, { tag: ['@skipWin'] },
homePage, async ({ app, editor, toolbar, scene }) => {
editor, test.skip(
toolbar, process.platform === 'win32',
scene, 'Fails on windows in CI, can not be replicated locally on windows.'
}) => { )
const file = await fs.readFile( const file = await app.getInputFile('e2e-can-sketch-on-chamfer.kcl')
path.resolve( await app.initialise(file)
__dirname,
'../../',
'./src/wasm-lib/tests/executor/inputs/e2e-can-sketch-on-chamfer.kcl'
),
'utf-8'
)
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene) const sketchOnAChamfer = _sketchOnAChamfer(app, editor, toolbar, scene)
await sketchOnAChamfer({ await sketchOnAChamfer({
clickCoords: { x: 570, y: 220 }, clickCoords: { x: 570, y: 220 },
cameraPos: { x: 16020, y: -2000, z: 10500 }, cameraPos: { x: 16020, y: -2000, z: 10500 },
cameraTarget: { x: -150, y: -4500, z: -80 }, cameraTarget: { x: -150, y: -4500, z: -80 },
beforeChamferSnippet: `angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01) beforeChamferSnippet: `angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)
chamfer({length = 30,tags = [ chamfer({length = 30,tags = [
seg01, seg01,
getNextAdjacentEdge(yo), getNextAdjacentEdge(yo),
@ -213,9 +175,10 @@ test.describe('verify sketch on chamfer works', () => {
getOppositeEdge(seg01) getOppositeEdge(seg01)
]}, %)`, ]}, %)`,
afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)', afterChamferSelectSnippet:
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', 'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002) - 90, segAng(rectangleSegmentA002) - 90,
105.26 105.26
@ -226,13 +189,13 @@ test.describe('verify sketch on chamfer works', () => {
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`, |> close(%)`,
}) })
await sketchOnAChamfer({ await sketchOnAChamfer({
clickCoords: { x: 690, y: 250 }, clickCoords: { x: 690, y: 250 },
cameraPos: { x: 16020, y: -2000, z: 10500 }, cameraPos: { x: 16020, y: -2000, z: 10500 },
cameraTarget: { x: -150, y: -4500, z: -80 }, cameraTarget: { x: -150, y: -4500, z: -80 },
beforeChamferSnippet: `angledLine([ beforeChamferSnippet: `angledLine([
segAng(rectangleSegmentA001) - 90, segAng(rectangleSegmentA001) - 90,
217.26 217.26
], %, $seg01)chamfer({ ], %, $seg01)chamfer({
@ -244,9 +207,10 @@ test.describe('verify sketch on chamfer works', () => {
] ]
}, %)`, }, %)`,
afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)', afterChamferSelectSnippet:
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', 'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA003) - 90, segAng(rectangleSegmentA003) - 90,
106.84 106.84
@ -257,21 +221,22 @@ test.describe('verify sketch on chamfer works', () => {
], %, $rectangleSegmentC002) ], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`, |> close(%)`,
}) })
await sketchOnAChamfer({ await sketchOnAChamfer({
clickCoords: { x: 677, y: 87 }, clickCoords: { x: 677, y: 87 },
cameraPos: { x: -6200, y: 1500, z: 6200 }, cameraPos: { x: -6200, y: 1500, z: 6200 },
cameraTarget: { x: 8300, y: 1100, z: 4800 }, cameraTarget: { x: 8300, y: 1100, z: 4800 },
beforeChamferSnippet: `angledLine([0, 268.43], %, $rectangleSegmentA001)chamfer({ beforeChamferSnippet: `angledLine([0, 268.43], %, $rectangleSegmentA001)chamfer({
length = 30, length = 30,
tags = [ tags = [
getNextAdjacentEdge(yo), getNextAdjacentEdge(yo),
getNextAdjacentEdge(seg02) getNextAdjacentEdge(seg02)
] ]
}, %)`, }, %)`,
afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)', afterChamferSelectSnippet:
afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)', 'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA003) - 90, segAng(rectangleSegmentA003) - 90,
106.84 106.84
@ -282,19 +247,20 @@ test.describe('verify sketch on chamfer works', () => {
], %, $rectangleSegmentC002) ], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`, |> close(%)`,
}) })
/// last one /// last one
await sketchOnAChamfer({ await sketchOnAChamfer({
clickCoords: { x: 620, y: 300 }, clickCoords: { x: 620, y: 300 },
cameraPos: { x: -1100, y: -7700, z: 1600 }, cameraPos: { x: -1100, y: -7700, z: 1600 },
cameraTarget: { x: 1450, y: 670, z: 4000 }, cameraTarget: { x: 1450, y: 670, z: 4000 },
beforeChamferSnippet: `chamfer({ beforeChamferSnippet: `chamfer({
length = 30, length = 30,
tags = [getNextAdjacentEdge(yo)] tags = [getNextAdjacentEdge(yo)]
}, %)`, }, %)`,
afterChamferSelectSnippet: 'sketch005 = startSketchOn(extrude001, seg06)', afterChamferSelectSnippet:
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)', 'sketch005 = startSketchOn(extrude001, seg06)',
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005) afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA005) - 90, segAng(rectangleSegmentA005) - 90,
@ -306,11 +272,11 @@ test.describe('verify sketch on chamfer works', () => {
], %, $rectangleSegmentC004) ], %, $rectangleSegmentC004)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`, |> close(%)`,
}) })
await test.step('verify at the end of the test that final code is what is expected', async () => { await test.step('verify at the end of the test that final code is what is expected', async () => {
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag] |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001) |> angledLine([0, 268.43], %, $rectangleSegmentA001)
@ -339,7 +305,7 @@ test.describe('verify sketch on chamfer works', () => {
tags = [getNextAdjacentEdge(yo)] tags = [getNextAdjacentEdge(yo)]
}, %, $seg06) }, %, $seg06)
sketch005 = startSketchOn(extrude001, seg06) sketch005 = startSketchOn(extrude001, seg06)
|> startProfileAt([-23.43,19.69], %) |> startProfileAt([-23.43, 19.69], %)
|> angledLine([0, 9.1], %, $rectangleSegmentA005) |> angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA005) - 90, segAng(rectangleSegmentA005) - 90,
@ -352,7 +318,7 @@ test.describe('verify sketch on chamfer works', () => {
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch004 = startSketchOn(extrude001, seg05) sketch004 = startSketchOn(extrude001, seg05)
|> startProfileAt([82.57,322.96], %) |> startProfileAt([82.57, 322.96], %)
|> angledLine([0, 11.16], %, $rectangleSegmentA004) |> angledLine([0, 11.16], %, $rectangleSegmentA004)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA004) - 90, segAng(rectangleSegmentA004) - 90,
@ -365,7 +331,7 @@ test.describe('verify sketch on chamfer works', () => {
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch003 = startSketchOn(extrude001, seg04) sketch003 = startSketchOn(extrude001, seg04)
|> startProfileAt([-209.64,255.28], %) |> startProfileAt([-209.64, 255.28], %)
|> angledLine([0, 11.56], %, $rectangleSegmentA003) |> angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA003) - 90, segAng(rectangleSegmentA003) - 90,
@ -378,7 +344,7 @@ test.describe('verify sketch on chamfer works', () => {
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch002 = startSketchOn(extrude001, seg03) sketch002 = startSketchOn(extrude001, seg03)
|> startProfileAt([205.96,254.59], %) |> startProfileAt([205.96, 254.59], %)
|> angledLine([0, 11.39], %, $rectangleSegmentA002) |> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002) - 90, segAng(rectangleSegmentA002) - 90,
@ -391,50 +357,43 @@ test.describe('verify sketch on chamfer works', () => {
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
`, `,
{ shouldNormalise: true } { shouldNormalise: true }
)
})
}
)
test(
'Works on chamfers that are non in a pipeExpression can break up multi edges in a chamfer array',
{ tag: ['@skipWin'] },
async ({ app, editor, toolbar, scene }) => {
test.skip(
process.platform === 'win32',
'Fails on windows in CI, can not be replicated locally on windows.'
) )
}) const file = await app.getInputFile(
}) 'e2e-can-sketch-on-chamfer-no-pipeExpr.kcl'
)
await app.initialise(file)
test('Works on chamfers that are non in a pipeExpression can break up multi edges in a chamfer array', async ({ const sketchOnAChamfer = _sketchOnAChamfer(app, editor, toolbar, scene)
context,
page,
homePage,
editor,
toolbar,
scene,
}) => {
const file = await fs.readFile(
path.resolve(
__dirname,
'../../',
'./src/wasm-lib/tests/executor/inputs/e2e-can-sketch-on-chamfer-no-pipeExpr.kcl'
),
'utf-8'
)
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene) await sketchOnAChamfer({
clickCoords: { x: 570, y: 220 },
await sketchOnAChamfer({ cameraPos: { x: 16020, y: -2000, z: 10500 },
clickCoords: { x: 570, y: 220 }, cameraTarget: { x: -150, y: -4500, z: -80 },
cameraPos: { x: 16020, y: -2000, z: 10500 }, beforeChamferSnippet: `angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)
cameraTarget: { x: -150, y: -4500, z: -80 },
beforeChamferSnippet: `angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)
chamfer({length=30,tags=[ chamfer({length=30,tags=[
seg01, seg01,
getNextAdjacentEdge(yo), getNextAdjacentEdge(yo),
getNextAdjacentEdge(seg02), getNextAdjacentEdge(seg02),
getOppositeEdge(seg01) getOppositeEdge(seg01)
]}, extrude001)`, ]}, extrude001)`,
beforeChamferSnippetEnd: '}, extrude001)', beforeChamferSnippetEnd: '}, extrude001)',
afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)', afterChamferSelectSnippet:
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', 'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002) - 90, segAng(rectangleSegmentA002) - 90,
105.26 105.26
@ -445,9 +404,9 @@ test.describe('verify sketch on chamfer works', () => {
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`, |> close(%)`,
}) })
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) |> startProfileAt([75.8, 317.2], %)
|> angledLine([0, 268.43], %, $rectangleSegmentA001) |> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
@ -487,56 +446,50 @@ sketch002 = startSketchOn(extrude001, seg03)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
`, `,
{ shouldNormalise: true } { shouldNormalise: true }
) )
}) }
)
}) })
test(`Verify axis, origin, and horizontal snapping`, async ({ test(`Verify axis, origin, and horizontal snapping`, async ({
page, app,
homePage,
editor, editor,
toolbar, toolbar,
scene, scene,
}) => { }) => {
const viewPortSize = { width: 1200, height: 500 }
await page.setBodyDimensions(viewPortSize)
await homePage.goToModelingScene()
// Constants and locators // Constants and locators
// These are mappings from screenspace to KCL coordinates, // These are mappings from screenspace to KCL coordinates,
// until we merge in our coordinate system helpers // until we merge in our coordinate system helpers
const xzPlane = [ const xzPlane = [
viewPortSize.width * 0.65, app.viewPortSize.width * 0.65,
viewPortSize.height * 0.3, app.viewPortSize.height * 0.3,
] as const ] as const
const originSloppy = { const originSloppy = {
screen: [ screen: [
viewPortSize.width / 2 + 3, // 3px off the center of the screen app.viewPortSize.width / 2 + 3, // 3px off the center of the screen
viewPortSize.height / 2, app.viewPortSize.height / 2,
], ],
kcl: [0, 0], kcl: [0, 0],
} as const } as const
const xAxisSloppy = { const xAxisSloppy = {
screen: [ screen: [
viewPortSize.width * 0.75, app.viewPortSize.width * 0.75,
viewPortSize.height / 2 - 3, // 3px off the X-axis app.viewPortSize.height / 2 - 3, // 3px off the X-axis
], ],
kcl: [20.34, 0], kcl: [16.95, 0],
} as const } as const
const offYAxis = { const offYAxis = {
screen: [ screen: [
viewPortSize.width * 0.6, // Well off the Y-axis, out of snapping range app.viewPortSize.width * 0.6, // Well off the Y-axis, out of snapping range
viewPortSize.height * 0.3, app.viewPortSize.height * 0.3,
], ],
kcl: [8.14, 6.78], kcl: [6.78, 6.78],
} as const } as const
const yAxisSloppy = { const yAxisSloppy = {
screen: [ screen: [
viewPortSize.width / 2 + 5, // 5px off the Y-axis app.viewPortSize.width / 2 + 5, // 5px off the Y-axis
viewPortSize.height * 0.3, app.viewPortSize.height * 0.3,
], ],
kcl: [0, 6.78], kcl: [0, 6.78],
} as const } as const
@ -557,13 +510,15 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`, afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
} }
await app.initialise()
await test.step(`Start a sketch on the XZ plane`, async () => { await test.step(`Start a sketch on the XZ plane`, async () => {
await editor.closePane() await editor.closePane()
await toolbar.startSketchPlaneSelection() await toolbar.startSketchPlaneSelection()
await moveToXzPlane() await moveToXzPlane()
await clickOnXzPlane() await clickOnXzPlane()
// timeout wait for engine animation is unavoidable // timeout wait for engine animation is unavoidable
await page.waitForTimeout(600) await app.page.waitForTimeout(600)
await editor.expectEditor.toContain(expectedCodeSnippets.sketchOnXzPlane) await editor.expectEditor.toContain(expectedCodeSnippets.sketchOnXzPlane)
}) })
await test.step(`Place a point a few pixels off the middle, verify it still snaps to 0,0`, async () => { await test.step(`Place a point a few pixels off the middle, verify it still snaps to 0,0`, async () => {
@ -597,127 +552,20 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
}) })
}) })
test(`Verify user can double-click to edit a sketch`, async ({
context,
page,
homePage,
editor,
toolbar,
scene,
}) => {
const u = await getUtils(page)
const initialCode = `closedSketch = startSketchOn('XZ')
|> circle({ center = [8, 5], radius = 2 }, %)
openSketch = startSketchOn('XY')
|> startProfileAt([-5, 0], %)
|> lineTo([0, 5], %)
|> xLine(5, %)
|> tangentialArcTo([10, 0], %)
`
const viewPortSize = { width: 1000, height: 500 }
await page.setBodyDimensions(viewPortSize)
await context.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
const pointInsideCircle = {
x: viewPortSize.width * 0.63,
y: viewPortSize.height * 0.5,
}
const pointOnPathAfterSketching = {
x: viewPortSize.width * 0.65,
y: viewPortSize.height * 0.5,
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_clickOpenPath, moveToOpenPath, dblClickOpenPath] =
scene.makeMouseHelpers(
pointOnPathAfterSketching.x,
pointOnPathAfterSketching.y
)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_clickCircle, moveToCircle, dblClickCircle] = scene.makeMouseHelpers(
pointInsideCircle.x,
pointInsideCircle.y
)
const exitSketch = async () => {
await test.step(`Exit sketch mode`, async () => {
await toolbar.exitSketchBtn.click()
await expect(toolbar.exitSketchBtn).not.toBeVisible()
await expect(toolbar.startSketchBtn).toBeEnabled()
})
}
await test.step(`Double-click on the closed sketch`, async () => {
await moveToCircle()
await dblClickCircle()
await expect(toolbar.startSketchBtn).not.toBeVisible()
await expect(toolbar.exitSketchBtn).toBeVisible()
await editor.expectState({
activeLines: [`|>circle({center=[8,5],radius=2},%)`],
highlightedCode: 'circle({center=[8,5],radius=2},%)',
diagnostics: [],
})
})
await page.waitForTimeout(1000)
await exitSketch()
await page.waitForTimeout(1000)
// Drag the sketch line out of the axis view which blocks the click
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: {
x: viewPortSize.width * 0.7,
y: viewPortSize.height * 0.5,
},
targetPosition: {
x: viewPortSize.width * 0.7,
y: viewPortSize.height * 0.4,
},
})
await page.waitForTimeout(500)
await test.step(`Double-click on the open sketch`, async () => {
await moveToOpenPath()
await scene.expectPixelColor([250, 250, 250], pointOnPathAfterSketching, 15)
// There is a full execution after exiting sketch that clears the scene.
await page.waitForTimeout(500)
await dblClickOpenPath()
await expect(toolbar.startSketchBtn).not.toBeVisible()
await expect(toolbar.exitSketchBtn).toBeVisible()
// Wait for enter sketch mode to complete
await page.waitForTimeout(500)
await editor.expectState({
activeLines: [`|>tangentialArcTo([10,0],%)`],
highlightedCode: 'tangentialArcTo([10,0],%)',
diagnostics: [],
})
})
})
test(`Offset plane point-and-click`, async ({ test(`Offset plane point-and-click`, async ({
context, app,
page,
homePage,
scene, scene,
editor, editor,
toolbar, toolbar,
cmdBar, cmdBar,
}) => { }) => {
await app.initialise()
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 150 } const testPoint = { x: 700, y: 150 }
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const expectedOutput = `plane001 = offsetPlane('XZ', 5)` const expectedOutput = `plane001 = offsetPlane('XZ', 5)`
await homePage.goToModelingScene()
await test.step(`Look for the blue of the XZ plane`, async () => { await test.step(`Look for the blue of the XZ plane`, async () => {
await scene.expectPixelColor([50, 51, 96], testPoint, 15) await scene.expectPixelColor([50, 51, 96], testPoint, 15)
}) })
@ -753,104 +601,3 @@ test(`Offset plane point-and-click`, async ({
await scene.expectPixelColor([74, 74, 74], testPoint, 15) await scene.expectPixelColor([74, 74, 74], testPoint, 15)
}) })
}) })
const loftPointAndClickCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },
]
loftPointAndClickCases.forEach(({ shouldPreselect }) => {
test(`Loft point-and-click (preselected sketches: ${shouldPreselect})`, async ({
context,
homePage,
page,
scene,
editor,
toolbar,
cmdBar,
}) => {
const u = await getUtils(page)
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50)
sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %)
`
await page.setBodyDimensions({ width: 1000, height: 500 })
await context.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(
testPoint.x,
testPoint.y + 80
)
const loftDeclaration = 'loft001 = loft([sketch001, sketch002])'
await test.step(`Look for the white of the sketch001 shape`, async () => {
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
async function selectSketches() {
await clickOnSketch1()
await page.keyboard.down('Shift')
await clickOnSketch2()
await page.waitForTimeout(500)
await page.keyboard.up('Shift')
}
if (!shouldPreselect) {
await test.step(`Go through the command bar flow without preselected sketches`, async () => {
await toolbar.loftButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: { Selection: '' },
highlightedHeaderArg: 'selection',
commandName: 'Loft',
})
await selectSketches()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Selection: '2 faces' },
commandName: 'Loft',
})
await cmdBar.progressCmdBar()
})
} else {
await test.step(`Preselect the two sketches`, async () => {
await selectSketches()
})
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
await toolbar.loftButton.click()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Selection: '2 faces' },
commandName: 'Loft',
})
await cmdBar.progressCmdBar()
})
}
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(loftDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [loftDeclaration],
highlightedCode: '',
})
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +1,46 @@
import { test, expect, Page } from './zoo-test' import { test, expect, Page } from '@playwright/test'
import path from 'path' import { join } from 'path'
import * as fsp from 'fs/promises' import * as fsp from 'fs/promises'
import { getUtils, executorInputPath } from './test-utils' import {
getUtils,
setup,
setupElectron,
tearDown,
executorInputPath,
} from './test-utils'
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates' import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Regression tests', () => { test.describe('Regression tests', () => {
// bugs we found that don't fit neatly into other categories // bugs we found that don't fit neatly into other categories
test('bad model has inline error #3251', async ({ test('bad model has inline error #3251', async ({ page }) => {
context,
page,
homePage,
}) => {
// because the model has `line([0,0]..` it is valid code, but the model is invalid // because the model has `line([0,0]..` it is valid code, but the model is invalid
// regression test for https://github.com/KittyCAD/modeling-app/issues/3251 // regression test for https://github.com/KittyCAD/modeling-app/issues/3251
// Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics // Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics
const u = await getUtils(page) const u = await getUtils(page)
await context.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch2 = startSketchOn("XY") `sketch2 = startSketchOn("XY")
sketch001 = startSketchAt([-0, -0]) sketch001 = startSketchAt([-0, -0])
|> line([0, 0], %) |> line([0, 0], %)
|> line([-4.84, -5.29], %) |> line([-4.84, -5.29], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)` |> close(%)`
) )
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.waitForPageLoad()
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
@ -47,7 +56,6 @@ test.describe('Regression tests', () => {
}) })
test('user should not have to press down twice in cmdbar', async ({ test('user should not have to press down twice in cmdbar', async ({
page, page,
homePage,
}) => { }) => {
// because the model has `line([0,0]..` it is valid code, but the model is invalid // because the model has `line([0,0]..` it is valid code, but the model is invalid
// regression test for https://github.com/KittyCAD/modeling-app/issues/3251 // regression test for https://github.com/KittyCAD/modeling-app/issues/3251
@ -56,38 +64,26 @@ test.describe('Regression tests', () => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XY') `sketch2 = startSketchOn("XY")
|> startProfileAt([82.33, 238.21], %) sketch001 = startSketchAt([-0, -0])
|> angledLine([0, 288.63], %, $rectangleSegmentA001) |> line([0, 0], %)
|> angledLine([ |> line([-4.84, -5.29], %)
segAng(rectangleSegmentA001) - 90,
197.97
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)`
extrude001 = extrude(50, sketch001)
`
) )
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await page.goto('/')
await u.waitForPageLoad() await u.waitForPageLoad()
await test.step('Check arrow down works', async () => { await test.step('Check arrow down works', async () => {
await page.getByTestId('command-bar-open-button').hover()
await page.getByTestId('command-bar-open-button').click() await page.getByTestId('command-bar-open-button').click()
const floppy = page.getByRole('option', { await page
name: 'floppy disk arrow Export', .getByRole('option', { name: 'floppy disk arrow Export' })
}) .click()
await floppy.click()
// press arrow down key twice // press arrow down key twice
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
@ -119,22 +115,21 @@ extrude001 = extrude(50, sketch001)
) )
}) })
}) })
test('executes on load', async ({ page, homePage }) => { test('executes on load', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('-XZ') `sketch001 = 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], %)`
) )
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.waitForPageLoad()
// expand variables section // expand variables section
const variablesTabButton = page.getByTestId('variables-pane-button') const variablesTabButton = page.getByTestId('variables-pane-button')
@ -153,15 +148,14 @@ extrude001 = extrude(50, sketch001)
).toBeVisible() ).toBeVisible()
}) })
test('re-executes', async ({ page, homePage }) => { test('re-executes', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem('persistCode', `myVar = 5`) localStorage.setItem('persistCode', `myVar = 5`)
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.waitForPageLoad()
const variablesTabButton = page.getByTestId('variables-pane-button') const variablesTabButton = page.getByTestId('variables-pane-button')
await variablesTabButton.click() await variablesTabButton.click()
@ -180,33 +174,32 @@ extrude001 = extrude(50, sketch001)
page.locator('.pretty-json-container >> text=myVar:67') page.locator('.pretty-json-container >> text=myVar:67')
).toBeVisible() ).toBeVisible()
}) })
test('ProgramMemory can be serialised', async ({ page, homePage }) => { test('ProgramMemory can be serialised', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`part = startSketchOn('XY') `part = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([0, 1], %) |> line([0, 1], %)
|> line([1, 0], %) |> line([1, 0], %)
|> line([0, -1], %) |> line([0, -1], %)
|> close(%) |> close(%)
|> extrude(1, %) |> extrude(1, %)
|> patternLinear3d({ |> patternLinear3d({
axis: [1, 0, 1], axis: [1, 0, 1],
repetitions: 3, repetitions: 3,
distance: 6 distance: 6
}, %)` }, %)`
) )
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
const messages: string[] = [] const messages: string[] = []
// Listen for all console events and push the message text to an array // Listen for all console events and push the message text to an array
page.on('console', (message) => messages.push(message.text())) page.on('console', (message) => messages.push(message.text()))
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.waitForPageLoad()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -219,26 +212,19 @@ extrude001 = extrude(50, sketch001)
}) })
}) })
}) })
test('ensure the Zoo logo is not a link in browser app', async ({ page }) => {
// Not relevant to us anymore, or at least for the time being.
test.skip('ensure the Zoo logo is not a link in browser app', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.waitForPageLoad()
const zooLogo = page.locator('[data-testid="app-logo"]') const zooLogo = page.locator('[data-testid="app-logo"]')
// Make sure it's not a link // Make sure it's not a link
await expect(zooLogo).not.toHaveAttribute('href') await expect(zooLogo).not.toHaveAttribute('href')
}) })
test( test(
'Position _ Is Out Of Range... regression test', 'Position _ Is Out Of Range... regression test',
{ tag: ['@skipWin'] }, { tag: ['@skipWin'] },
async ({ context, page, homePage }) => { async ({ page }) => {
// SKip on windows, its being weird. // SKip on windows, its being weird.
test.skip( test.skip(
process.platform === 'win32', process.platform === 'win32',
@ -247,26 +233,25 @@ extrude001 = extrude(50, sketch001)
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await context.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`exampleSketch = startSketchOn("XZ") `exampleSketch = startSketchOn("XZ")
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> angledLine({ angle: 50, length: 45 }, %) |> angledLine({ angle: 50, length: 45 }, %)
|> yLineTo(0, %) |> yLineTo(0, %)
|> close(%) |> close(%)
|> |>
example = extrude(5, exampleSketch) example = extrude(5, exampleSketch)
shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)` shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)`
) )
}) })
await expect(async () => { await expect(async () => {
await homePage.goToModelingScene() await page.goto('/')
await u.waitForPageLoad() await u.waitForPageLoad()
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible({ await expect(page.locator('.cm-lint-marker-error')).toBeVisible({
timeout: 1_000, timeout: 1_000,
@ -308,12 +293,12 @@ extrude001 = extrude(50, sketch001)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toContainText(`exampleSketch = startSketchOn("XZ") .toContainText(`exampleSketch = startSketchOn("XZ")
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> angledLine({ angle: 50, length: 45 }, %) |> angledLine({ angle: 50, length: 45 }, %)
|> yLineTo(0, %) |> yLineTo(0, %)
|> close(%) |> close(%)
thing: "blah"`) thing: "blah"`)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
} }
@ -321,7 +306,6 @@ extrude001 = extrude(50, sketch001)
test('when engine fails export we handle the failure and alert the user', async ({ test('when engine fails export we handle the failure and alert the user', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript( await page.addInitScript(
@ -332,10 +316,9 @@ extrude001 = extrude(50, sketch001)
{ code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } { code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR }
) )
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.waitForPageLoad()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -391,6 +374,7 @@ extrude001 = extrude(50, sketch001)
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
await u.clearCommandLogs()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
@ -424,7 +408,7 @@ extrude001 = extrude(50, sketch001)
test( test(
'ensure you can not export while an export is already going', 'ensure you can not export while an export is already going',
{ tag: ['@skipLinux', '@skipWin'] }, { tag: ['@skipLinux', '@skipWin'] },
async ({ page, homePage }) => { async ({ page }) => {
// This is being weird on ubuntu and windows. // This is being weird on ubuntu and windows.
test.skip( test.skip(
// eslint-disable-next-line jest/valid-title // eslint-disable-next-line jest/valid-title
@ -444,10 +428,9 @@ extrude001 = extrude(50, sketch001)
} }
) )
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.waitForPageLoad()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -517,17 +500,20 @@ extrude001 = extrude(50, sketch001)
test( test(
`Network health indicator only appears in modeling view`, `Network health indicator only appears in modeling view`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ browserName: _ }, testInfo) => {
await context.folderSetupFn(async (dir) => { const { electronApp, page } = await setupElectron({
const bracketDir = path.join(dir, 'bracket') testInfo,
await fsp.mkdir(bracketDir, { recursive: true }) folderSetupFn: async (dir) => {
await fsp.copyFile( const bracketDir = join(dir, 'bracket')
executorInputPath('focusrite_scarlett_mounting_braket.kcl'), await fsp.mkdir(bracketDir, { recursive: true })
path.join(bracketDir, 'main.kcl') await fsp.copyFile(
) executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
},
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page) const u = await getUtils(page)
// Locators // Locators
@ -553,17 +539,18 @@ extrude001 = extrude(50, sketch001)
await u.waitForPageLoad() await u.waitForPageLoad()
await expect(networkHealthIndicator).toContainText('Connected') await expect(networkHealthIndicator).toContainText('Connected')
}) })
await electronApp.close()
} }
) )
test(`View gizmo stays visible even when zoomed out all the way`, async ({ test(`View gizmo stays visible even when zoomed out all the way`, async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
// Constants and locators // Constants and locators
const planeColor: [number, number, number] = [161, 220, 155] const planeColor: [number, number, number] = [170, 220, 170]
const bgColor: [number, number, number] = [27, 27, 27] const bgColor: [number, number, number] = [27, 27, 27]
const middlePixelIsColor = async (color: [number, number, number]) => { const middlePixelIsColor = async (color: [number, number, number]) => {
return u.getGreatestPixDiff({ x: 600, y: 250 }, color) return u.getGreatestPixDiff({ x: 600, y: 250 }, color)
@ -574,9 +561,8 @@ extrude001 = extrude(50, sketch001)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem('persistCode', '') localStorage.setItem('persistCode', '')
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.waitForPageLoad()
await u.closeKclCodePanel() await u.closeKclCodePanel()
}) })

View File

@ -7,8 +7,6 @@ try {
.split('\n') .split('\n')
.filter((line) => line && line.length > 1) .filter((line) => line && line.length > 1)
.forEach((line) => { .forEach((line) => {
// Allow line comments.
if (line.trimStart().startsWith('#')) return
const [key, value] = line.split('=') const [key, value] = line.split('=')
// prefer env vars over secrets file // prefer env vars over secrets file
secrets[key] = process.env[key] || (value as any).replaceAll('"', '') secrets[key] = process.env[key] || (value as any).replaceAll('"', '')

File diff suppressed because it is too large Load Diff

View File

@ -47,12 +47,7 @@ test.beforeEach(async ({ page }) => {
test.setTimeout(60_000) test.setTimeout(60_000)
test(
// We test this end to end already - getting this to work on web just to take
// a snapshot of it feels weird. I'd rather our regular tests fail.
// The primary failure is doExport now relies on the filesystem. We can follow
// up with another PR if we want this back.
test.skip(
'exports of each format should work', 'exports of each format should work',
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] }, { tag: ['@snapshot', '@skipWin', '@skipMacos'] },
async ({ page, context }) => { async ({ page, context }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -109,21 +109,242 @@ keychain = startSketchOn("XY")
|> close(%) |> close(%)
|> extrude(thickness, %) |> extrude(thickness, %)
keychain1 = startSketchOn("XY") // generated from /home/paultag/Downloads/zma-logomark.svg
|> startProfileAt([0, 0], %) fn svg = (surface, origin, depth) => {
|> lineTo([width, 0], %) let a0 = surface |> startProfileAt([origin[0] + 45.430427, origin[1] + -14.627736], %)
|> lineTo([width, height], %) |> bezierCurve({
|> lineTo([0, height], %) control1: [ 0, 0.764157 ],
|> close(%) control2: [ 0, 1.528314 ],
|> extrude(thickness, %) to: [ 0, 2.292469 ]
}, %)
|> bezierCurve({
control1: [ -3.03202, 0 ],
control2: [ -6.064039, 0 ],
to: [ -9.09606, 0 ]
}, %)
|> bezierCurve({
control1: [ 0, -1.077657 ],
control2: [ 0, -2.155312 ],
to: [ 0, -3.232969 ]
}, %)
|> bezierCurve({
control1: [ 2.741805, 0 ],
control2: [ 5.483613, 0 ],
to: [ 8.225417, 0 ]
}, %)
|> bezierCurve({
control1: [ -2.740682, -2.961815 ],
control2: [ -5.490342, -5.925794 ],
to: [ -8.225417, -8.886255 ]
}, %)
|> bezierCurve({
control1: [ 0, -0.723995 ],
control2: [ 0, -1.447988 ],
to: [ 0, -2.171981 ]
}, %)
|> bezierCurve({
control1: [ 0.712124, 0.05061 ],
control2: [ 1.511636, -0.09877 ],
to: [ 2.172096, 0.07005 ]
}, %)
|> bezierCurve({
control1: [ 0.68573, 0.740811 ],
control2: [ 1.371459, 1.481622 ],
to: [ 2.057187, 2.222436 ]
}, %)
|> bezierCurve({
control1: [ 0, -0.76416 ],
control2: [ 0, -1.52832 ],
to: [ 0, -2.29248 ]
}, %)
|> bezierCurve({
control1: [ 3.032013, 0 ],
control2: [ 6.064026, 0 ],
to: [ 9.096038, 0 ]
}, %)
|> bezierCurve({
control1: [ 0, 1.077657 ],
control2: [ 0, 2.155314 ],
to: [ 0, 3.232973 ]
}, %)
|> bezierCurve({
control1: [ -2.741312, 0 ],
control2: [ -5.482623, 0 ],
to: [ -8.223936, 0 ]
}, %)
|> bezierCurve({
control1: [ 2.741313, 2.961108 ],
control2: [ 5.482624, 5.922216 ],
to: [ 8.223936, 8.883325 ]
}, %)
|> bezierCurve({
control1: [ 0, 0.724968 ],
control2: [ 0, 1.449938 ],
to: [ 0, 2.174907 ]
}, %)
|> bezierCurve({
control1: [ -0.712656, -0.05145 ],
control2: [ -1.512554, 0.09643 ],
to: [ -2.173592, -0.07298 ]
}, %)
|> bezierCurve({
control1: [ -0.685222, -0.739834 ],
control2: [ -1.370445, -1.479669 ],
to: [ -2.055669, -2.219505 ]
}, %)
|> close(%)
|> extrude(depth, %)
keychain2 = startSketchOn("XY") let a1 = surface |> startProfileAt([origin[0] + 57.920488, origin[1] + -15.244943], %)
|> startProfileAt([0, 0], %) |> bezierCurve({
|> lineTo([width, 0], %) control1: [ -2.78904, 0.106635 ],
|> lineTo([width, height], %) control2: [ -5.052548, -2.969529 ],
|> lineTo([0, height], %) to: [ -4.055141, -5.598369 ]
|> close(%) }, %)
|> extrude(thickness, %) |> bezierCurve({
control1: [ 0.841523, -0.918736 ],
control2: [ 0.439412, -1.541892 ],
to: [ -0.368488, -2.214378 ]
}, %)
|> bezierCurve({
control1: [ -0.418245, -0.448461 ],
control2: [ -0.836489, -0.896922 ],
to: [ -1.254732, -1.345384 ]
}, %)
|> bezierCurve({
control1: [ -2.76806, 2.995359 ],
control2: [ -2.32667, 8.18409 ],
to: [ 0.897655, 10.678932 ]
}, %)
|> bezierCurve({
control1: [ 2.562822, 2.186098 ],
control2: [ 6.605111, 2.28043 ],
to: [ 9.271202, 0.226476 ]
}, %)
|> bezierCurve({
control1: [ -0.743744, -0.797465 ],
control2: [ -1.487487, -1.594932 ],
to: [ -2.231232, -2.392397 ]
}, %)
|> bezierCurve({
control1: [ -0.672938, 0.421422 ],
control2: [ -1.465362, 0.646946 ],
to: [ -2.259264, 0.64512 ]
}, %)
|> close(%)
|> extrude(depth, %)
let a2 = surface |> startProfileAt([origin[0] + 62.19406300000001, origin[1] + -19.500698999999997], %)
|> bezierCurve({
control1: [ 0.302938, 1.281141 ],
control2: [ -1.53575, 2.434288 ],
to: [ -0.10908, 3.279477 ]
}, %)
|> bezierCurve({
control1: [ 0.504637, 0.54145 ],
control2: [ 1.009273, 1.082899 ],
to: [ 1.513909, 1.624348 ]
}, %)
|> bezierCurve({
control1: [ 2.767778, -2.995425 ],
control2: [ 2.327135, -8.184384 ],
to: [ -0.897661, -10.679047 ]
}, %)
|> bezierCurve({
control1: [ -2.562947, -2.186022 ],
control2: [ -6.604089, -2.279606 ],
to: [ -9.271196, -0.227813 ]
}, %)
|> bezierCurve({
control1: [ 0.744231, 0.797952 ],
control2: [ 1.488461, 1.595904 ],
to: [ 2.232692, 2.393856 ]
}, %)
|> bezierCurve({
control1: [ 2.302377, -1.564629 ],
control2: [ 5.793126, -0.15358 ],
to: [ 6.396577, 2.547372 ]
}, %)
|> bezierCurve({
control1: [ 0.08981, 0.346302 ],
control2: [ 0.134865, 0.704078 ],
to: [ 0.13476, 1.061807 ]
}, %)
|> close(%)
|> extrude(depth, %)
let a3 = surface |> startProfileAt([origin[0] + 74.124866, origin[1] + -15.244943], %)
|> bezierCurve({
control1: [ -2.78904, 0.106635 ],
control2: [ -5.052549, -2.969529 ],
to: [ -4.055142, -5.598369 ]
}, %)
|> bezierCurve({
control1: [ 0.841527, -0.918738 ],
control2: [ 0.43941, -1.541892 ],
to: [ -0.368497, -2.214367 ]
}, %)
|> bezierCurve({
control1: [ -0.418254, -0.448466 ],
control2: [ -0.836507, -0.896931 ],
to: [ -1.254761, -1.345395 ]
}, %)
|> bezierCurve({
control1: [ -2.768019, 2.995371 ],
control2: [ -2.326624, 8.184088 ],
to: [ 0.897678, 10.678932 ]
}, %)
|> bezierCurve({
control1: [ 2.56289, 2.186191 ],
control2: [ 6.60516, 2.280307 ],
to: [ 9.271371, 0.226476 ]
}, %)
|> bezierCurve({
control1: [ -0.743808, -0.797465 ],
control2: [ -1.487616, -1.594932 ],
to: [ -2.231424, -2.392397 ]
}, %)
|> bezierCurve({
control1: [ -0.672916, 0.421433 ],
control2: [ -1.465344, 0.646926 ],
to: [ -2.259225, 0.64512 ]
}, %)
|> close(%)
|> extrude(depth, %)
let a4 = surface |> startProfileAt([origin[0] + 77.57333899999998, origin[1] + -16.989262999999998], %)
|> bezierCurve({
control1: [ 0.743298, 0.797463 ],
control2: [ 1.486592, 1.594926 ],
to: [ 2.229888, 2.392389 ]
}, %)
|> bezierCurve({
control1: [ 2.767827, -2.995393 ],
control2: [ 2.327103, -8.184396 ],
to: [ -0.897672, -10.679047 ]
}, %)
|> bezierCurve({
control1: [ -2.562939, -2.186037 ],
control2: [ -6.604077, -2.279589 ],
to: [ -9.271185, -0.227813 ]
}, %)
|> bezierCurve({
control1: [ 0.744243, 0.797952 ],
control2: [ 1.488486, 1.595904 ],
to: [ 2.232729, 2.393856 ]
}, %)
|> bezierCurve({
control1: [ 2.302394, -1.564623 ],
control2: [ 5.793201, -0.153598 ],
to: [ 6.396692, 2.547372 ]
}, %)
|> bezierCurve({
control1: [ 0.32074, 1.215468 ],
control2: [ 0.06159, 2.564765 ],
to: [ -0.690452, 3.573243 ]
}, %)
|> close(%)
|> extrude(depth, %)
box = startSketchOn('XY') box = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
@ -133,7 +354,7 @@ box = startSketchOn('XY')
|> close(%) |> close(%)
|> extrude(10, %) |> extrude(10, %)
sketch001 = startSketchOn(box, revolveAxis) sketch001 = startSketchOn(box, revolveAxis)
|> startProfileAt([5, 10], %) |> startProfileAt([5, 10], %)
|> line([0, -10], %) |> line([0, -10], %)
|> line([2, 0], %) |> line([2, 0], %)
@ -143,12 +364,18 @@ sketch001 = startSketchOn(box, revolveAxis)
axis: revolveAxis, axis: revolveAxis,
angle: 90 angle: 90
}, %) }, %)
return 0
}
sketch001 = startSketchOn('XZ')
|> startProfileAt([0.0, 0.0], %)
|> xLine(0.0, %)
|> close(%)
`
svg(startSketchOn(keychain, 'end'), [-33, 32], -thickness)
startSketchOn(keychain, 'end')
|> circle({ center: [
width / 2,
height - (keychainHoleSize + 1.5)
], radius: keychainHoleSize }, %)
|> extrude(-thickness, %)`
export const TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR = `thing = 1` export const TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR = `thing = 1`

View File

@ -1,16 +1,29 @@
import { test, expect } from './zoo-test' import { test, expect } from '@playwright/test'
import { commonPoints, getUtils } from './test-utils' import { commonPoints, getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Test network and connection issues', () => { test.describe('Test network and connection issues', () => {
test('simulate network down and network little widget', async ({ test('simulate network down and network little widget', async ({
page, page,
homePage, browserName,
}) => { }) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
test.skip(
browserName === 'webkit',
'Skip on Safari until `window.tearDown` is working there'
)
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
const networkToggle = page.getByTestId('network-toggle') const networkToggle = page.getByTestId('network-toggle')
@ -49,7 +62,7 @@ test.describe('Test network and connection issues', () => {
}) })
// Expect the network to be down // Expect the network to be down
await expect(networkToggle).toContainText('Problem') await expect(networkToggle).toContainText('Offline')
// Click the network widget // Click the network widget
await networkWidget.click() await networkWidget.click()
@ -80,19 +93,26 @@ test.describe('Test network and connection issues', () => {
test('Engine disconnect & reconnect in sketch mode', async ({ test('Engine disconnect & reconnect in sketch mode', async ({
page, page,
homePage, browserName,
}) => { }) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit // TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
test.skip(
browserName === 'webkit',
'Skip on Safari until `window.tearDown` is working there'
)
const networkToggle = page.getByTestId('network-toggle') const networkToggle = page.getByTestId('network-toggle')
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await homePage.goToModelingScene() await u.waitForAuthSkipAppStart()
await u.waitForPageLoad()
await u.openDebugPanel() await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// click on "Start Sketch" button // click on "Start Sketch" button
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
@ -112,7 +132,7 @@ test.describe('Test network and connection issues', () => {
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(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
@ -120,8 +140,8 @@ test.describe('Test network and connection issues', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)`) |> xLine(${commonPoints.num1}, %)`)
// Expect the network to be up // Expect the network to be up
await expect(networkToggle).toContainText('Connected') await expect(networkToggle).toContainText('Connected')
@ -136,7 +156,7 @@ test.describe('Test network and connection issues', () => {
}) })
// Expect the network to be down // Expect the network to be down
await expect(networkToggle).toContainText('Problem') await expect(networkToggle).toContainText('Offline')
// Ensure we are not in sketch mode // Ensure we are not in sketch mode
await expect( await expect(

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