Compare commits

...

48 Commits

Author SHA1 Message Date
99c62bf0f0 Revert "Switch projects fix (#3310)"
This reverts commit dff3848a00.
2024-08-07 13:50:59 +02:00
ee20d4781f Revert "Don't set firstRender from within a routeLoader (#3311)"
This reverts commit 4bfbecd3e7.
2024-08-07 13:50:40 +02:00
1e80738b2a Revert "Persist theme - Reload everything on a disconnect (#3250)"
This reverts commit 3f082c8222.
2024-08-07 13:50:26 +02:00
308a0fb06e Revert "Cut release v0.24.10" (#3313)
Revert "Cut release v0.24.10 (#3309)"

This reverts commit 214ae6f512.
2024-08-07 11:47:21 +00:00
214ae6f512 Cut release v0.24.10 (#3309)
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-08-07 07:13:45 -04:00
8d54fec589 fix test: Basic sketch › code pane open at start (#3188)
* added 1 extra hard coded timeout

* stabilize test code pane open at start

* re-apply changes

---------

Co-authored-by: ryanrosello-og <ry@zoo.dev>
2024-08-07 21:10:58 +10:00
4bfbecd3e7 Don't set firstRender from within a routeLoader (#3311) 2024-08-07 06:21:15 -04:00
dff3848a00 Switch projects fix (#3310)
* switch projects fix

* add comment
2024-08-07 19:47:23 +10:00
f875efab1b Split large (flow-tests) spec file into individual spec file per test suite (#3297)
* split flow tests spec file
---------

Co-authored-by: ryanrosello-og <ry@zoo.dev>
2024-08-07 19:27:32 +10:00
3f082c8222 Persist theme - Reload everything on a disconnect (#3250)
* Reload everything on a disconnect

* fix unit-integration tests

* Further improvements to connection manager; persist theme across reconnects

* Fix up artifactGraph.test

* Actually pass the callback

* Kurt hmmm (#3308)

* kurts attempts

* we're almost sane

* get tests working, praise be

---------

Co-authored-by: 49lf <ircsurfer33@gmail.com>

* typo

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2024-08-07 17:11:57 +10:00
e1c45bdb33 Start to rework some of our kcl docs (#3222)
* Start to rework some of our kcl docs

Signed-off-by: Paul R. Tagliamonte <paul@zoo.dev>
2024-08-06 20:27:26 -04:00
5cb5dbd689 artifactGraph snapshot stability (#3305)
* artifactgraph snapshot stability

* clean up

* tweak

* Look at this (photo)Graph *in the voice of Nickelback*

* trigger ci

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-07 10:15:08 +10:00
0a8881bc69 badge scale on hover (#3298) 2024-08-07 07:29:16 +10:00
be9438160e bisect docs (#3304) 2024-08-07 06:27:23 +10:00
77b565f781 Add a search bar to the projects/home page (#3301)
* Add a search bar to the projects/home page

* Better hotkey config

* Look at this (photo)Graph *in the voice of Nickelback*

* Re-run CI

* Look at this (photo)Graph *in the voice of Nickelback*

* Re-run CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-06 16:19:30 -04:00
c843dfad95 Add a little dropdown arrow menu to gizmo with view settings (#3300) 2024-08-06 10:01:55 -04:00
a3ff0a45eb Disable build-test-web's tests on release or nightly build (#3296) 2024-08-06 05:50:49 -04:00
4617ad0fed Cut release v0.24.9 (#3295) 2024-08-06 05:30:34 -04:00
5fa51a2f92 Revert "Cut release v0.24.9"
This reverts commit 4218777afb.
2024-08-06 17:34:11 +10:00
4218777afb Cut release v0.24.9 2024-08-06 17:32:39 +10:00
8b1b462e29 Revert "Cut release v0.24.9 (#3284)" (#3294)
This reverts commit 1c778bf373.
2024-08-06 17:32:18 +10:00
2bc99ba39b reset camera on empty scene (#3293)
* reset camera on empty scene

* tweak numbers

* number tweak again
2024-08-06 17:05:46 +10:00
ffe0da6dcd don't allow edit on sketches with no variable declaration (#3292)
don't allow edit on sketches with no variable decleration
2024-08-06 06:17:30 +00:00
d27afb8c74 Improve docs for x/yLine and x/yLineTo (#3285)
* Improve docs for xLineTo and yLineTo

* Improve docs for xLine and yLine
2024-08-06 14:26:19 +10:00
1c778bf373 Cut release v0.24.9 (#3284)
* Cut release v0.24.9

* Look at this (photo)Graph *in the voice of Nickelback*

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-06 13:13:34 +10:00
5df8a943a9 Easy KCL tests with no engine/visuals (#3282)
This new test framework, `no_visuals`, is for testing KCL programs via asserts, not via twenty-twenty visual tests. This is useful for unit-testing small fragments of KCL.

It's easy! All you need to do is:
- Write a KCL file
- Save it under `tests/executor/inputs/no_visuals/foo.kcl`
- Open `no_visuals.rs` and add `gen_test!(foo);`
2024-08-06 02:44:49 +00:00
ab729dbcdb Bump google-github-actions/upload-cloud-storage from 2.1.0 to 2.1.1 (#3272)
Bumps [google-github-actions/upload-cloud-storage](https://github.com/google-github-actions/upload-cloud-storage) from 2.1.0 to 2.1.1.
- [Release notes](https://github.com/google-github-actions/upload-cloud-storage/releases)
- [Changelog](https://github.com/google-github-actions/upload-cloud-storage/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/upload-cloud-storage/compare/v2.1.0...v2.1.1)

---
updated-dependencies:
- dependency-name: google-github-actions/upload-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 10:13:00 -07:00
84865eaed0 Add assertEqual function to KCL stdlib (#3279)
Takes a tolerance, because floating-point imprecision.
2024-08-05 16:31:58 +00:00
543e809739 Add "report a bug" mention to user menu onboarding step (#3278)
* Mention "report a bug" in user menu onboarding step

* Add to a test so we match QAWolf a bit more

* Re-run CI
2024-08-05 08:24:19 -04:00
61b669cf4e github actions Playwright shard execution (#3199)
* add @snapshot tag to all snapshot-tests

* set workers to 1 on CI

* try sharding on google chrome only

* add retry when navigating to the app (on CI)

* reduce ubuntu-cores to 2

* revert runner back to 8 cores

* re-add retry + enable macos

* Reduce timeouts to 30 minutes

* ensure retry is triggered

* removed sharding when retrying failures

* added --pass-with-no-tests

* ensure failure occurs

* revert failure

* use smaller sized runners

* revert back to supported runner size

* revert to og version

* minimize changes

* yarn fmt

* ensure failure

* undo failure

---------

Co-authored-by: ryanrosello-og <ry@zoo.dev>
2024-08-05 21:30:16 +10:00
75f1aaa824 camera breaks on extrude zoom to fit (#3276) 2024-08-05 16:08:30 +10:00
f4848d7dea Jump to error not lint (#3271)
* dont jump to lints

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-05 02:44:33 +00:00
a0167f6ba6 Coplanar sketch should have diagnostic error. (#3269)
* artifactMapTweak

* typo
2024-08-05 02:04:53 +00:00
e5a26c42e6 Cut release v0.24.8 (#3263) 2024-08-05 10:32:16 +10:00
9c87b124d9 Jump to error code on badge click (#3262)
* add function to scroll to view

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* scroll into view on click

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add test for jump to code with error

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-04 22:59:04 +00:00
21389c089d apply fillets before a shell (#3261)
* add test for fillet and shell

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* apply fillets before a shell

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-04 15:37:40 -07:00
29f57be8c1 editor repaints any errors when rendered (#3260)
* editor repaints any errors when rendered

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Update src/lang/KclSingleton.ts

* fix test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix typo

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-08-04 15:16:34 -07:00
cd55f07619 Bump serde_json from 1.0.121 to 1.0.122 in /src/wasm-lib (#3235)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.121 to 1.0.122.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.121...v1.0.122)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-04 12:57:54 -07:00
baf7d3dd9d Add print button (#3133)
* add print button

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* generate more types

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add a github action to generate machine api-types

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* New machine-api types

* actually print on the real machine

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* add more

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* New machine-api types

* get the current machine

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* New machine-api types

* know when error

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fmt

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add fmt

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* New machine-api types

* empty

* empty

* update machine api

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* New machine-api types

* empty

* New machine-api types

* emptuy

* no circular deps

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* New machine-api types

* remove recursive dep

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-08-04 04:51:30 +00:00
54a9a50969 fix bug when engine returns an error on websocket export (#3256)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-04 02:24:06 +00:00
2830c750fa remove unused timeout (#3254) 2024-08-03 23:10:04 +00:00
d3160cd85a Update machine-api spec (#3253)
YOYO NEW API SPEC!

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-03 16:07:42 -07:00
fd1b4c3a32 fix snapshot tests, don't let them silently fail (#3228)
fix snapshots, don't let them silently fail
2024-08-03 22:29:28 +00:00
b0a41c31ac Update machine-api spec (#3252)
YOYO NEW API SPEC!

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-03 15:25:02 -07:00
5825ba575c test for default planes in empty scene (#3249)
* test for default planes in empty scene

* fmt

* skit if webkit

* fmt
2024-08-03 21:34:56 +10:00
e5bec2140e ArtifactGraph reThink (PART 3) (#3140)
* adjust engine connection to opt out of webRTC connection

* refactor start and test setup

* add env to unit test

* spell config update

* fix beforeAll order bug

* initial integration of new artifact map with tests passing

* remove old artifact map and clean up

* graph artifact map

* have graph commited

* have graph commited

* remove bad file

* install playwright

* fmt

* commit permissions

* typo

* flesh out tests more

* Look at this (photo)Graph *in the voice of Nickelback*

* multi highlight

* redo image logic

* add in solid 2d data into artifactMap

* fix snapshots

* stabiles graph images

* Look at this (photo)Graph *in the voice of Nickelback*

* tweak tests

* rename blend to edgeCut

* Look at this (photo)Graph *in the voice of Nickelback*

* fix playw tests

* start of artifact map rename to graph

* rename file

* rename test

* rename clearup

* comments

* docs

* docs proof read

* few tweaks here and there

* typos

* delete get parent logic

* nit, combine if statements

* remove unused param

* fix silly test bug

* rename surfId to sufaceId

* rename types

* update comments

* add comment

* add extra check

* Look at this (photo)Graph *in the voice of Nickelback*

* pull out merge artifact function

* update comments

* fix test

* fmt

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-03 18:08:51 +10:00
7bf6bc3048 Fix computed properties of KCL objects (#3246)
* Fix computed properties of KCL objects

Fixes https://github.com/KittyCAD/modeling-app/issues/3201

* Incorporate Jon's feedback
2024-08-02 22:24:00 -07:00
22f9df73ed Update machine-api spec (#3247)
YOYO NEW API SPEC!

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-02 21:08:20 -07:00
203 changed files with 15829 additions and 10300 deletions

View File

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

View File

@ -20,6 +20,11 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
actions: read
jobs:
check-format:
runs-on: 'ubuntu-latest'
@ -84,8 +89,43 @@ jobs:
- run: yarn build:wasm
- run: yarn simpleserver:ci
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
- name: Install Chromium Browser
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: yarn playwright install chromium --with-deps
- name: run unit tests
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: yarn test:nowatch
env:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: check for changes
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
id: git-check
run: |
git add src/lang/std/artifactMapGraphs
if git status src/lang/std/artifactMapGraphs | grep -q "Changes to be committed"
then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes, if any
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' && steps.git-check.outputs.modified == 'true' }}
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
# TODO when webkit works on ubuntu remove the os part of the commit message
git commit -am "Look at this (photo)Graph *in the voice of Nickelback*" || true
git push
git push origin ${{ github.head_ref }}
- run: yarn test:nowatch
prepare-json-files:
@ -495,7 +535,7 @@ jobs:
project_id: kittycadapi
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: artifact
glob: '*/Zoo*'
@ -503,13 +543,13 @@ jobs:
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: last_download.json
destination: ${{ env.BUCKET_DIR }}

View File

@ -0,0 +1,49 @@
name: generate machine-api types
on:
pull_request:
paths:
- 'openapi/machine-api.json'
- '.github/workflows/generate-machine-api-types.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: write
jobs:
generate:
runs-on: 'ubuntu-latest'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn generate:machine-api
- run: yarn fmt
- name: check for changes
id: git-check
run: |
git add .
if git status | grep -q "Changes to be committed"
then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes, if any
if: steps.git-check.outputs.modified == 'true'
run: |
git add .
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
git commit -am "New machine-api types" || true
git push
git push origin ${{ github.head_ref }}

View File

@ -13,7 +13,7 @@ permissions:
contents: write
pull-requests: write
actions: read
jobs:
@ -34,8 +34,13 @@ jobs:
- 'src/wasm-lib/**'
playwright-ubuntu:
timeout-minutes: 60
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
needs: check-rust-changes
steps:
- name: Tune GitHub-hosted runner network
@ -106,13 +111,19 @@ jobs:
- name: build web
run: yarn build:local
- name: Run ubuntu/chrome snapshots
continue-on-error: true
run: |
yarn playwright test --project="Google Chrome" --update-snapshots e2e/playwright/snapshot-tests.spec.ts
yarn playwright test --project="Google Chrome" --retries="3" --update-snapshots --grep=@snapshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-ubuntu-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true
- name: Clean up test-results
if: always()
continue-on-error: true
@ -143,7 +154,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: steps.git-check.outputs.modified == 'true'
with:
name: playwright-report-ubuntu-${{ github.sha }}
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
# if have previous run results, use them
@ -151,7 +162,7 @@ jobs:
if: always()
continue-on-error: true
with:
name: test-results-ubuntu-${{ github.sha }}
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
- name: Run ubuntu/chrome flow (with retries)
id: retry
@ -160,14 +171,14 @@ jobs:
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts || true
yarn playwright test --project="Google Chrome" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true
# # 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
@ -175,7 +186,7 @@ jobs:
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="Google Chrome" --last-failed e2e/playwright/flow-tests.spec.ts || true
yarn playwright test --project="Google Chrome" --last-failed --grep-invert=@snapshot || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
@ -188,9 +199,9 @@ jobs:
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
@ -210,21 +221,26 @@ jobs:
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-ubuntu-${{ github.sha }}
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-ubuntu-${{ github.sha }}
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true
playwright-macos:
timeout-minutes: 60
runs-on: macos-14-large
timeout-minutes: 30
runs-on: macos-14
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
needs: check-rust-changes
steps:
- name: Tune GitHub-hosted runner network
@ -300,7 +316,7 @@ jobs:
if: ${{ always() }}
continue-on-error: true
with:
name: test-results-macos-${{ github.sha }}
name: test-results-macos-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
- name: Run macos/safari flow (with retries)
id: retry
@ -309,14 +325,14 @@ jobs:
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts || true
yarn playwright test --project="webkit" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true
# # 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
@ -324,7 +340,7 @@ jobs:
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="webkit" --last-failed e2e/playwright/flow-tests.spec.ts || true
yarn playwright test --project="webkit" --last-failed --grep-invert=@snapshot || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
@ -337,9 +353,9 @@ jobs:
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
@ -354,15 +370,14 @@ jobs:
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: test-results-macos-${{ github.sha }}
name: test-results-macos-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: playwright-report-macos-${{ github.sha }}
name: playwright-report-macos-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true

2
.gitignore vendored
View File

@ -39,6 +39,7 @@ src/wasm-lib/grackle/test_json_output
e2e/playwright/playwright-secrets.env
e2e/playwright/temp1.png
e2e/playwright/temp2.png
e2e/playwright/temp3.png
# exports from snapshot-tests.spec.ts "exports of each format should work"
e2e/playwright/export-snapshots/*
!e2e/playwright/export-snapshots/*.png
@ -48,6 +49,7 @@ e2e/playwright/export-snapshots/*
/playwright-report/
/blob-report/
/playwright/.cache/
/src/lang/std/artifactMapCache
## generated files

View File

@ -110,6 +110,17 @@ Note that these became separate apps on Macos, so make sure you open the right o
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
## Checking out commits / Bisecting
Which commands from setup are one off vs need to be run every time?
The following will need to be run when checking out a new commit and guarantees the build is not stale:
```bash
yarn install
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build
yarn start # or yarn build:local && yarn serve for slower but more production-like build
```
## Before submitting a PR
Before you submit a contribution PR to this repo, please ensure that:

View File

@ -1,10 +1,10 @@
---
title: "abs"
excerpt: "Computes the absolute value of a number."
excerpt: "Compute the absolute value of a number."
layout: manual
---
Computes the absolute value of a number.
Compute the absolute value of a number.

View File

@ -1,10 +1,10 @@
---
title: "acos"
excerpt: "Computes the arccosine of a number (in radians)."
excerpt: "Compute the arccosine of a number (in radians)."
layout: manual
---
Computes the arccosine of a number (in radians).
Compute the arccosine of a number (in radians).

View File

@ -1,10 +1,10 @@
---
title: "angleToMatchLengthX"
excerpt: "Returns the angle to match the given length for x."
excerpt: "Compute the angle (in degrees) in o"
layout: manual
---
Returns the angle to match the given length for x.
Compute the angle (in degrees) in o

View File

@ -1,12 +1,12 @@
---
title: "angledLine"
excerpt: "Draw an angled line."
excerpt: "Draw a line segment relative to the current origin using the polar"
layout: manual
---
Draw an angled line.
Draw a line segment relative to the current origin using the polar
measure of some angle and distance.
```js
angledLine(data: AngledLineData, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,12 +1,12 @@
---
title: "angledLineOfXLength"
excerpt: "Draw an angled line of a given x length."
excerpt: "Create a line segment from the current 2-dimensional sketch origin"
layout: manual
---
Draw an angled line of a given x length.
Create a line segment from the current 2-dimensional sketch origin
along some angle (in degrees) for some relative length in the 'x' dimension.
```js
angledLineOfXLength(data: AngledLineData, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,12 +1,12 @@
---
title: "angledLineOfYLength"
excerpt: "Draw an angled line of a given y length."
excerpt: "Create a line segment from the current 2-dimensional sketch origin"
layout: manual
---
Draw an angled line of a given y length.
Create a line segment from the current 2-dimensional sketch origin
along some angle (in degrees) for some relative length in the 'y' dimension.
```js
angledLineOfYLength(data: AngledLineData, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,12 +1,12 @@
---
title: "angledLineThatIntersects"
excerpt: "Draw an angled line that intersects with a given line."
excerpt: "Draw an angled line from the current origin, constructing a line segment"
layout: manual
---
Draw an angled line that intersects with a given line.
Draw an angled line from the current origin, constructing a line segment
such that the newly created line intersects the desired target line segment.
```js
angledLineThatIntersects(data: AngledLineThatIntersectsData, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,12 +1,12 @@
---
title: "angledLineToX"
excerpt: "Draw an angled line to a given x coordinate."
excerpt: "Create a line segment from the current 2-dimensional sketch origin"
layout: manual
---
Draw an angled line to a given x coordinate.
Create a line segment from the current 2-dimensional sketch origin
along some angle (in degrees) for some length, ending at the provided value in the 'x' dimension.
```js
angledLineToX(data: AngledLineToData, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,12 +1,12 @@
---
title: "angledLineToY"
excerpt: "Draw an angled line to a given y coordinate."
excerpt: "Create a line segment from the current 2-dimensional sketch origin"
layout: manual
---
Draw an angled line to a given y coordinate.
Create a line segment from the current 2-dimensional sketch origin
along some angle (in degrees) for some length, ending at the provided value in the 'y' dimension.
```js
angledLineToY(data: AngledLineToData, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,12 +1,14 @@
---
title: "arc"
excerpt: "Draw an arc."
excerpt: "Starting at the current sketch's origin, draw a curved line segment along"
layout: manual
---
Draw an arc.
Starting at the current sketch's origin, draw a curved line segment along
an imaginary circle of the specified radius.
The arc is constructed such that the current position of the sketch is placed along an imaginary circle of the specified radius, at angleStart degrees. The resulting arc is the segment of the imaginary circle from that origin point to angleEnd, radius away from the center of the imaginary circle.
Unless this makes a lot of sense and feels like what you're looking for to construct your shape, you're likely looking for tangentialArc.
```js
arc(data: ArcData, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,10 +1,10 @@
---
title: "asin"
excerpt: "Computes the arcsine of a number (in radians)."
excerpt: "Compute the arcsine of a number (in radians)."
layout: manual
---
Computes the arcsine of a number (in radians).
Compute the arcsine of a number (in radians).

37
docs/kcl/assertEqual.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,10 @@
---
title: "atan"
excerpt: "Computes the arctangent of a number (in radians)."
excerpt: "Compute the arctangent of a number (in radians)."
layout: manual
---
Computes the arctangent of a number (in radians).
Compute the arctangent of a number (in radians).

View File

@ -1,12 +1,12 @@
---
title: "bezierCurve"
excerpt: "Draw a bezier curve."
excerpt: "Draw a smooth, continuous, curved line segment from the current origin to"
layout: manual
---
Draw a bezier curve.
Draw a smooth, continuous, curved line segment from the current origin to
the desired (x, y), using a number of control points to shape the curve's shape.
```js
bezierCurve(data: BezierData, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,10 +1,10 @@
---
title: "ceil"
excerpt: "Computes the smallest integer greater than or equal to a number."
excerpt: "Compute the smallest integer greater than or equal to a number."
layout: manual
---
Computes the smallest integer greater than or equal to a number.
Compute the smallest integer greater than or equal to a number.

View File

@ -1,12 +1,12 @@
---
title: "chamfer"
excerpt: "Create chamfers on tagged paths."
excerpt: "Cut a straight transitional edge along a tagged path."
layout: manual
---
Create chamfers on tagged paths.
Cut a straight transitional edge along a tagged path.
Chamfer is similar in function and use to a fillet, except a fillet will blend the transition along an edge, rather than cut a sharp, straight transitional edge.
```js
chamfer(data: ChamferData, extrude_group: ExtrudeGroup, tag?: TagDeclarator) -> ExtrudeGroup

View File

@ -1,12 +1,12 @@
---
title: "circle"
excerpt: "Sketch a circle."
excerpt: "Construct a 2-dimensional circle, of the specified radius, centered at"
layout: manual
---
Sketch a circle.
Construct a 2-dimensional circle, of the specified radius, centered at
the provided (x, y) origin point.
```js
circle(center: [number], radius: number, sketch_surface_or_group: SketchSurfaceOrGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,12 +1,12 @@
---
title: "close"
excerpt: "Close the current sketch."
excerpt: "Construct a line segment from the current origin back to the profile's"
layout: manual
---
Close the current sketch.
Construct a line segment from the current origin back to the profile's
origin, ensuring the resulting 2-dimensional sketch is not open-ended.
```js
close(sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,10 +1,10 @@
---
title: "cos"
excerpt: "Computes the cosine of a number (in radians)."
excerpt: "Compute the cosine of a number (in radians)."
layout: manual
---
Computes the cosine of a number (in radians).
Compute the cosine of a number (in radians).

View File

@ -1,12 +1,12 @@
---
title: "extrude"
excerpt: "Extrudes by a given amount."
excerpt: "Extend a 2-dimensional sketch through a third dimension in order to"
layout: manual
---
Extrudes by a given amount.
Extend a 2-dimensional sketch through a third dimension in order to
create new 3-dimensional volume, or if extruded into an existing volume, cut into an existing solid.
```js
extrude(length: number, sketch_group_set: SketchGroupSet) -> ExtrudeGroupSet

View File

@ -1,12 +1,12 @@
---
title: "fillet"
excerpt: "Create fillets on tagged paths."
excerpt: "Blend a transitional edge along a tagged path, smoothing the sharp edge."
layout: manual
---
Create fillets on tagged paths.
Blend a transitional edge along a tagged path, smoothing the sharp edge.
Fillet is similar in function and use to a chamfer, except a chamfer will cut a sharp transition along an edge while fillet will smoothly blend the transition.
```js
fillet(data: FilletData, extrude_group: ExtrudeGroup, tag?: TagDeclarator) -> ExtrudeGroup

View File

@ -1,10 +1,10 @@
---
title: "floor"
excerpt: "Computes the largest integer less than or equal to a number."
excerpt: "Compute the largest integer less than or equal to a number."
layout: manual
---
Computes the largest integer less than or equal to a number.
Compute the largest integer less than or equal to a number.

View File

@ -1,10 +1,10 @@
---
title: "hole"
excerpt: "Use a sketch to cut a hole in another sketch."
excerpt: "Use a 2-dimensional sketch to cut a hole in another 2-dimensional sketch."
layout: manual
---
Use a sketch to cut a hole in another sketch.
Use a 2-dimensional sketch to cut a hole in another 2-dimensional sketch.

View File

@ -6,8 +6,8 @@ layout: manual
Import a CAD file.
For formats lacking unit data (STL, OBJ, PLY), the default import unit is millimeters. Otherwise you can specify the unit by passing in the options parameter. If you import a gltf file, we will try to find the bin file and import it as well.
Import paths are relative to the current project directory. This only works in the desktop app not in browser.
For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.
Note: The import command currently only works when using the native Modeling App.
```js
import(file_path: String, options?: ImportFormat) -> ImportedGeometry

View File

@ -21,6 +21,7 @@ layout: manual
* [`arc`](kcl/arc)
* [`asin`](kcl/asin)
* [`assert`](kcl/assert)
* [`assertEqual`](kcl/assertEqual)
* [`assertGreaterThan`](kcl/assertGreaterThan)
* [`assertGreaterThanOrEq`](kcl/assertGreaterThanOrEq)
* [`assertLessThan`](kcl/assertLessThan)

View File

@ -1,10 +1,10 @@
---
title: "int"
excerpt: "Converts a number to an integer."
excerpt: "Convert a number to an integer."
layout: manual
---
Converts a number to an integer.
Convert a number to an integer.
Callers should use floor(), ceil(), or other rounding function first if they care about how numbers with fractional parts are converted. If the number has a fractional part, it's truncated, moving the number towards zero.
If the number is NaN or has a magnitude, either positive or negative, that is too large to fit into the internal integer representation, the result is a runtime error.

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,12 @@
---
title: "lastSegY"
excerpt: "Returns the last segment of y."
excerpt: "Extract the 'y' axis value of the last line segment in the provided 2-d"
layout: manual
---
Returns the last segment of y.
Extract the 'y' axis value of the last line segment in the provided 2-d
sketch.
```js
lastSegY(sketch_group: SketchGroup) -> number

View File

@ -1,10 +1,10 @@
---
title: "legAngX"
excerpt: "Returns the angle of the given leg for x."
excerpt: "Compute the angle of the given leg for x."
layout: manual
---
Returns the angle of the given leg for x.
Compute the angle of the given leg for x.

View File

@ -1,10 +1,10 @@
---
title: "legAngY"
excerpt: "Returns the angle of the given leg for y."
excerpt: "Compute the angle of the given leg for y."
layout: manual
---
Returns the angle of the given leg for y.
Compute the angle of the given leg for y.

View File

@ -1,10 +1,10 @@
---
title: "legLen"
excerpt: "Returns the length of the given leg."
excerpt: "Compute the length of the given leg."
layout: manual
---
Returns the length of the given leg.
Compute the length of the given leg.

View File

@ -1,12 +1,12 @@
---
title: "line"
excerpt: "Draw a line."
excerpt: "Draw a line relative to the current origin to a specified (x, y) away"
layout: manual
---
Draw a line.
Draw a line relative to the current origin to a specified (x, y) away
from the current position.
```js
line(delta: [number], sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,10 +1,10 @@
---
title: "lineTo"
excerpt: "Draw a line to a point."
excerpt: "Draw a line from the current origin to some absolute (x, y) point."
layout: manual
---
Draw a line to a point.
Draw a line from the current origin to some absolute (x, y) point.

View File

@ -1,10 +1,10 @@
---
title: "ln"
excerpt: "Computes the natural logarithm of the number."
excerpt: "Compute the natural logarithm of the number."
layout: manual
---
Computes the natural logarithm of the number.
Compute the natural logarithm of the number.

View File

@ -1,10 +1,10 @@
---
title: "log"
excerpt: "Computes the logarithm of the number with respect to an arbitrary base."
excerpt: "Compute the logarithm of the number with respect to an arbitrary base."
layout: manual
---
Computes the logarithm of the number with respect to an arbitrary base.
Compute the logarithm of the number with respect to an arbitrary base.
The result might not be correctly rounded owing to implementation details; `log2()` can produce more accurate results for base 2, and `log10()` can produce more accurate results for base 10.

View File

@ -1,10 +1,10 @@
---
title: "log10"
excerpt: "Computes the base 10 logarithm of the number."
excerpt: "Compute the base 10 logarithm of the number."
layout: manual
---
Computes the base 10 logarithm of the number.
Compute the base 10 logarithm of the number.

View File

@ -1,10 +1,10 @@
---
title: "log2"
excerpt: "Computes the base 2 logarithm of the number."
excerpt: "Compute the base 2 logarithm of the number."
layout: manual
---
Computes the base 2 logarithm of the number.
Compute the base 2 logarithm of the number.

View File

@ -1,10 +1,10 @@
---
title: "max"
excerpt: "Computes the maximum of the given arguments."
excerpt: "Compute the maximum of the given arguments."
layout: manual
---
Computes the maximum of the given arguments.
Compute the maximum of the given arguments.

View File

@ -1,10 +1,10 @@
---
title: "min"
excerpt: "Computes the minimum of the given arguments."
excerpt: "Compute the minimum of the given arguments."
layout: manual
---
Computes the minimum of the given arguments.
Compute the minimum of the given arguments.

View File

@ -1,12 +1,12 @@
---
title: "patternCircular2d"
excerpt: "A circular pattern on a 2D sketch."
excerpt: "Repeat a 2-dimensional sketch some number of times along a partial or"
layout: manual
---
A circular pattern on a 2D sketch.
Repeat a 2-dimensional sketch some number of times along a partial or
complete circle some specified number of times. Each object may additionally be rotated along the circle, ensuring orentation of the solid with respect to the center of the circle is maintained.
```js
patternCircular2d(data: CircularPattern2dData, sketch_group_set: SketchGroupSet) -> [SketchGroup]

View File

@ -1,12 +1,12 @@
---
title: "patternCircular3d"
excerpt: "A circular pattern on a 3D model."
excerpt: "Repeat a 3-dimensional solid some number of times along a partial or"
layout: manual
---
A circular pattern on a 3D model.
Repeat a 3-dimensional solid some number of times along a partial or
complete circle some specified number of times. Each object may additionally be rotated along the circle, ensuring orentation of the solid with respect to the center of the circle is maintained.
```js
patternCircular3d(data: CircularPattern3dData, extrude_group_set: ExtrudeGroupSet) -> [ExtrudeGroup]

View File

@ -1,12 +1,12 @@
---
title: "patternLinear2d"
excerpt: "A linear pattern on a 2D sketch."
excerpt: "Repeat a 2-dimensional sketch along some dimension, with a dynamic amount"
layout: manual
---
A linear pattern on a 2D sketch.
Repeat a 2-dimensional sketch along some dimension, with a dynamic amount
of distance between each repetition, some specified number of times.
```js
patternLinear2d(data: LinearPattern2dData, sketch_group_set: SketchGroupSet) -> [SketchGroup]

View File

@ -1,12 +1,12 @@
---
title: "patternLinear3d"
excerpt: "A linear pattern on a 3D model."
excerpt: "Repeat a 3-dimensional solid along a linear path, with a dynamic amount"
layout: manual
---
A linear pattern on a 3D model.
Repeat a 3-dimensional solid along a linear path, with a dynamic amount
of distance between each repetition, some specified number of times.
```js
patternLinear3d(data: LinearPattern3dData, extrude_group_set: ExtrudeGroupSet) -> [ExtrudeGroup]

View File

@ -1,12 +1,12 @@
---
title: "patternTransform"
excerpt: "A linear pattern on a 3D solid."
excerpt: "Repeat a 3-dimensional solid by successively applying a transformation (such"
layout: manual
---
A linear pattern on a 3D solid.
Repeat a 3-dimensional solid by successively applying a transformation (such
Each repetition of the pattern can be transformed (e.g. scaled, translated, hidden, etc).
as rotation, scale, translation, visibility) on each repetition.
```js
patternTransform(num_repetitions: u32, transform_function: FunctionParam, extrude_group_set: ExtrudeGroupSet) -> [ExtrudeGroup]

View File

@ -1,12 +1,12 @@
---
title: "polar"
excerpt: "Convert from polar/sphere coordinates to cartesian coordinates."
excerpt: "Convert polar/sphere (azimuth, elevation, distance) coordinates to"
layout: manual
---
Convert from polar/sphere coordinates to cartesian coordinates.
Convert polar/sphere (azimuth, elevation, distance) coordinates to
cartesian (x/y/z grid) coordinates.
```js
polar(data: PolarCoordsData) -> [number]

View File

@ -1,10 +1,10 @@
---
title: "pow"
excerpt: "Computes the number to a power."
excerpt: "Compute the number to a power."
layout: manual
---
Computes the number to a power.
Compute the number to a power.

View File

@ -1,12 +1,12 @@
---
title: "profileStart"
excerpt: ""
excerpt: "Extract the provided 2-dimensional sketch group's profile's origin"
layout: manual
---
Extract the provided 2-dimensional sketch group's profile's origin
value.
```js
profileStart(sketch_group: SketchGroup) -> [number]

View File

@ -1,12 +1,12 @@
---
title: "profileStartX"
excerpt: ""
excerpt: "Extract the provided 2-dimensional sketch group's profile's origin's 'x'"
layout: manual
---
Extract the provided 2-dimensional sketch group's profile's origin's 'x'
value.
```js
profileStartX(sketch_group: SketchGroup) -> number

View File

@ -1,12 +1,12 @@
---
title: "profileStartY"
excerpt: ""
excerpt: "Extract the provided 2-dimensional sketch group's profile's origin's 'y'"
layout: manual
---
Extract the provided 2-dimensional sketch group's profile's origin's 'y'
value.
```js
profileStartY(sketch_group: SketchGroup) -> number

View File

@ -1,12 +1,12 @@
---
title: "revolve"
excerpt: "Revolve a sketch around an axis."
excerpt: "Rotate a sketch around some provided axis, creating a solid from its extent."
layout: manual
---
Revolve a sketch around an axis.
Rotate a sketch around some provided axis, creating a solid from its extent.
This, like extrude, is able to create a 3-dimensional solid from a 2-dimensional sketch. However, unlike extrude, this creates a solid by using the extent of the sketch as its revolved around an axis rather than using the extent of the sketch linearly translated through a third dimension.
```js
revolve(data: RevolveData, sketch_group: SketchGroup) -> ExtrudeGroup

View File

@ -1,10 +1,10 @@
---
title: "segAng"
excerpt: "Returns the angle of the segment."
excerpt: "Compute the angle (in degrees) of the provided line segment."
layout: manual
---
Returns the angle of the segment.
Compute the angle (in degrees) of the provided line segment.

View File

@ -1,10 +1,10 @@
---
title: "segEndX"
excerpt: "Returns the segment end of x."
excerpt: "Compute the ending point of the provided line segment along the 'x' axis."
layout: manual
---
Returns the segment end of x.
Compute the ending point of the provided line segment along the 'x' axis.

View File

@ -1,10 +1,10 @@
---
title: "segEndY"
excerpt: "Returns the segment end of y."
excerpt: "Compute the ending point of the provided line segment along the 'y' axis."
layout: manual
---
Returns the segment end of y.
Compute the ending point of the provided line segment along the 'y' axis.

View File

@ -1,10 +1,10 @@
---
title: "segLen"
excerpt: "Returns the length of the segment."
excerpt: "Compute the length of the provided line segment."
layout: manual
---
Returns the length of the segment.
Compute the length of the provided line segment.

View File

@ -1,12 +1,12 @@
---
title: "shell"
excerpt: "Shell a solid."
excerpt: "Remove volume from a 3-dimensional shape such that a wall of the"
layout: manual
---
Shell a solid.
Remove volume from a 3-dimensional shape such that a wall of the
provided thickness remains, taking volume starting at the provided face, leaving it open in that direction.
```js
shell(data: ShellData, extrude_group: ExtrudeGroup) -> ExtrudeGroup

View File

@ -1,10 +1,10 @@
---
title: "sin"
excerpt: "Computes the sine of a number (in radians)."
excerpt: "Compute the sine of a number (in radians)."
layout: manual
---
Computes the sine of a number (in radians).
Compute the sine of a number (in radians).

View File

@ -1,10 +1,10 @@
---
title: "sqrt"
excerpt: "Computes the square root of a number."
excerpt: "Compute the square root of a number."
layout: manual
---
Computes the square root of a number.
Compute the square root of a number.

View File

@ -1,10 +1,10 @@
---
title: "startProfileAt"
excerpt: "Start a profile at a given point."
excerpt: "Start a new profile at a given point."
layout: manual
---
Start a profile at a given point.
Start a new profile at a given point.

View File

@ -1,10 +1,10 @@
---
title: "startSketchAt"
excerpt: "Start a sketch at a given point on the 'XY' plane."
excerpt: "Start a new 2-dimensional sketch at a given point on the 'XY' plane."
layout: manual
---
Start a sketch at a given point on the 'XY' plane.
Start a new 2-dimensional sketch at a given point on the 'XY' plane.

View File

@ -1,10 +1,10 @@
---
title: "startSketchOn"
excerpt: "Start a sketch on a specific plane or face."
excerpt: "Start a new 2-dimensional sketch on a specific plane or face."
layout: manual
---
Start a sketch on a specific plane or face.
Start a new 2-dimensional sketch on a specific plane or face.

View File

@ -1,7 +1,7 @@
[
{
"name": "abs",
"summary": "Computes the absolute value of a number.",
"summary": "Compute the absolute value of a number.",
"description": "",
"tags": [
"math"
@ -34,7 +34,7 @@
},
{
"name": "acos",
"summary": "Computes the arccosine of a number (in radians).",
"summary": "Compute the arccosine of a number (in radians).",
"description": "",
"tags": [
"math"
@ -67,7 +67,7 @@
},
{
"name": "angleToMatchLengthX",
"summary": "Returns the angle to match the given length for x.",
"summary": "Compute the angle (in degrees) in o",
"description": "",
"tags": [],
"args": [
@ -7623,8 +7623,8 @@
},
{
"name": "angledLine",
"summary": "Draw an angled line.",
"description": "",
"summary": "Draw a line segment relative to the current origin using the polar",
"description": "measure of some angle and distance.",
"tags": [],
"args": [
{
@ -14288,8 +14288,8 @@
},
{
"name": "angledLineOfXLength",
"summary": "Draw an angled line of a given x length.",
"description": "",
"summary": "Create a line segment from the current 2-dimensional sketch origin",
"description": "along some angle (in degrees) for some relative length in the 'x' dimension.",
"tags": [],
"args": [
{
@ -20953,8 +20953,8 @@
},
{
"name": "angledLineOfYLength",
"summary": "Draw an angled line of a given y length.",
"description": "",
"summary": "Create a line segment from the current 2-dimensional sketch origin",
"description": "along some angle (in degrees) for some relative length in the 'y' dimension.",
"tags": [],
"args": [
{
@ -27618,8 +27618,8 @@
},
{
"name": "angledLineThatIntersects",
"summary": "Draw an angled line that intersects with a given line.",
"description": "",
"summary": "Draw an angled line from the current origin, constructing a line segment",
"description": "such that the newly created line intersects the desired target line segment.",
"tags": [],
"args": [
{
@ -34726,8 +34726,8 @@
},
{
"name": "angledLineToX",
"summary": "Draw an angled line to a given x coordinate.",
"description": "",
"summary": "Create a line segment from the current 2-dimensional sketch origin",
"description": "along some angle (in degrees) for some length, ending at the provided value in the 'x' dimension.",
"tags": [],
"args": [
{
@ -41376,8 +41376,8 @@
},
{
"name": "angledLineToY",
"summary": "Draw an angled line to a given y coordinate.",
"description": "",
"summary": "Create a line segment from the current 2-dimensional sketch origin",
"description": "along some angle (in degrees) for some length, ending at the provided value in the 'y' dimension.",
"tags": [],
"args": [
{
@ -48026,8 +48026,8 @@
},
{
"name": "arc",
"summary": "Draw an arc.",
"description": "",
"summary": "Starting at the current sketch's origin, draw a curved line segment along",
"description": "an imaginary circle of the specified radius.\nThe arc is constructed such that the current position of the sketch is placed along an imaginary circle of the specified radius, at angleStart degrees. The resulting arc is the segment of the imaginary circle from that origin point to angleEnd, radius away from the center of the imaginary circle.\nUnless this makes a lot of sense and feels like what you're looking for to construct your shape, you're likely looking for tangentialArc.",
"tags": [],
"args": [
{
@ -54723,7 +54723,7 @@
},
{
"name": "asin",
"summary": "Computes the arcsine of a number (in radians).",
"summary": "Compute the arcsine of a number (in radians).",
"description": "",
"tags": [
"math"
@ -54791,6 +54791,62 @@
"const myVar = true\nassert(myVar, \"should always be true\")"
]
},
{
"name": "assertEqual",
"summary": "Check that a numerical value equals another at runtime,",
"description": "otherwise raise an error.",
"tags": [],
"args": [
{
"name": "left",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
{
"name": "right",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
{
"name": "epsilon",
"type": "number",
"schema": {
"type": "number",
"format": "double"
},
"required": true
},
{
"name": "message",
"type": "string",
"schema": {
"type": "string"
},
"required": true
}
],
"returnValue": {
"name": "",
"type": "()",
"schema": {
"type": "null"
},
"required": true
},
"unpublished": false,
"deprecated": false,
"examples": [
"let n = 1.0285\nlet m = 1.0286\nassertEqual(n, m, 0.01, \"n is within the given tolerance for m\")"
]
},
{
"name": "assertGreaterThan",
"summary": "Check that a numerical value is greater than another at runtime,",
@ -54981,7 +55037,7 @@
},
{
"name": "atan",
"summary": "Computes the arctangent of a number (in radians).",
"summary": "Compute the arctangent of a number (in radians).",
"description": "",
"tags": [
"math"
@ -55014,8 +55070,8 @@
},
{
"name": "bezierCurve",
"summary": "Draw a bezier curve.",
"description": "",
"summary": "Draw a smooth, continuous, curved line segment from the current origin to",
"description": "the desired (x, y), using a number of control points to shape the curve's shape.",
"tags": [],
"args": [
{
@ -61685,7 +61741,7 @@
},
{
"name": "ceil",
"summary": "Computes the smallest integer greater than or equal to a number.",
"summary": "Compute the smallest integer greater than or equal to a number.",
"description": "",
"tags": [
"math"
@ -61718,8 +61774,8 @@
},
{
"name": "chamfer",
"summary": "Create chamfers on tagged paths.",
"description": "",
"summary": "Cut a straight transitional edge along a tagged path.",
"description": "Chamfer is similar in function and use to a fillet, except a fillet will blend the transition along an edge, rather than cut a sharp, straight transitional edge.",
"tags": [],
"args": [
{
@ -67380,8 +67436,8 @@
},
{
"name": "circle",
"summary": "Sketch a circle.",
"description": "",
"summary": "Construct a 2-dimensional circle, of the specified radius, centered at",
"description": "the provided (x, y) origin point.",
"tags": [],
"args": [
{
@ -74839,8 +74895,8 @@
},
{
"name": "close",
"summary": "Close the current sketch.",
"description": "",
"summary": "Construct a line segment from the current origin back to the profile's",
"description": "origin, ensuring the resulting 2-dimensional sketch is not open-ended.",
"tags": [],
"args": [
{
@ -81465,7 +81521,7 @@
},
{
"name": "cos",
"summary": "Computes the cosine of a number (in radians).",
"summary": "Compute the cosine of a number (in radians).",
"description": "",
"tags": [
"math"
@ -81521,8 +81577,8 @@
},
{
"name": "extrude",
"summary": "Extrudes by a given amount.",
"description": "",
"summary": "Extend a 2-dimensional sketch through a third dimension in order to",
"description": "create new 3-dimensional volume, or if extruded into an existing volume, cut into an existing solid.",
"tags": [],
"args": [
{
@ -91533,8 +91589,8 @@
},
{
"name": "fillet",
"summary": "Create fillets on tagged paths.",
"description": "",
"summary": "Blend a transitional edge along a tagged path, smoothing the sharp edge.",
"description": "Fillet is similar in function and use to a chamfer, except a chamfer will cut a sharp transition along an edge while fillet will smoothly blend the transition.",
"tags": [],
"args": [
{
@ -97195,7 +97251,7 @@
},
{
"name": "floor",
"summary": "Computes the largest integer less than or equal to a number.",
"summary": "Compute the largest integer less than or equal to a number.",
"description": "",
"tags": [
"math"
@ -103844,7 +103900,7 @@
},
{
"name": "hole",
"summary": "Use a sketch to cut a hole in another sketch.",
"summary": "Use a 2-dimensional sketch to cut a hole in another 2-dimensional sketch.",
"description": "",
"tags": [],
"args": [
@ -115789,7 +115845,7 @@
{
"name": "import",
"summary": "Import a CAD file.",
"description": "For formats lacking unit data (STL, OBJ, PLY), the default import unit is millimeters. Otherwise you can specify the unit by passing in the options parameter. If you import a gltf file, we will try to find the bin file and import it as well.\nImport paths are relative to the current project directory. This only works in the desktop app not in browser.",
"description": "For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.\nNote: The import command currently only works when using the native Modeling App.",
"tags": [],
"args": [
{
@ -116445,7 +116501,7 @@
},
{
"name": "int",
"summary": "Converts a number to an integer.",
"summary": "Convert a number to an integer.",
"description": "Callers should use floor(), ceil(), or other rounding function first if they care about how numbers with fractional parts are converted. If the number has a fractional part, it's truncated, moving the number towards zero.\nIf the number is NaN or has a magnitude, either positive or negative, that is too large to fit into the internal integer representation, the result is a runtime error.",
"tags": [
"convert"
@ -116478,8 +116534,8 @@
},
{
"name": "lastSegX",
"summary": "Returns the last segment of x.",
"description": "",
"summary": "Extract the 'x' axis value of the last line segment in the provided 2-d",
"description": "sketch.",
"tags": [],
"args": [
{
@ -119781,13 +119837,13 @@
"unpublished": false,
"deprecated": false,
"examples": [
"const exampleSketch = startSketchOn(\"XZ\")\n |> startProfileAt([0, 0], %)\n |> line([5, 0], %)\n |> line([20, 5], %)\n |> line([0, lastSegX(%)], %)\n |> line([-15, 0], %)\n |> close(%)\n\nconst example = extrude(5, exampleSketch)"
"const exampleSketch = startSketchOn(\"XZ\")\n |> startProfileAt([0, 0], %)\n |> line([5, 0], %)\n |> line([20, 5], %)\n |> line([lastSegX(%), 0], %)\n |> line([-15, 0], %)\n |> close(%)\n\nconst example = extrude(5, exampleSketch)"
]
},
{
"name": "lastSegY",
"summary": "Returns the last segment of y.",
"description": "",
"summary": "Extract the 'y' axis value of the last line segment in the provided 2-d",
"description": "sketch.",
"tags": [],
"args": [
{
@ -123094,7 +123150,7 @@
},
{
"name": "legAngX",
"summary": "Returns the angle of the given leg for x.",
"summary": "Compute the angle of the given leg for x.",
"description": "",
"tags": [
"utilities"
@ -123136,7 +123192,7 @@
},
{
"name": "legAngY",
"summary": "Returns the angle of the given leg for y.",
"summary": "Compute the angle of the given leg for y.",
"description": "",
"tags": [
"utilities"
@ -123178,7 +123234,7 @@
},
{
"name": "legLen",
"summary": "Returns the length of the given leg.",
"summary": "Compute the length of the given leg.",
"description": "",
"tags": [
"utilities"
@ -123220,8 +123276,8 @@
},
{
"name": "line",
"summary": "Draw a line.",
"description": "",
"summary": "Draw a line relative to the current origin to a specified (x, y) away",
"description": "from the current position.",
"tags": [],
"args": [
{
@ -129860,7 +129916,7 @@
},
{
"name": "lineTo",
"summary": "Draw a line to a point.",
"summary": "Draw a line from the current origin to some absolute (x, y) point.",
"description": "",
"tags": [],
"args": [
@ -136499,7 +136555,7 @@
},
{
"name": "ln",
"summary": "Computes the natural logarithm of the number.",
"summary": "Compute the natural logarithm of the number.",
"description": "",
"tags": [
"math"
@ -136532,7 +136588,7 @@
},
{
"name": "log",
"summary": "Computes the logarithm of the number with respect to an arbitrary base.",
"summary": "Compute the logarithm of the number with respect to an arbitrary base.",
"description": "The result might not be correctly rounded owing to implementation details; `log2()` can produce more accurate results for base 2, and `log10()` can produce more accurate results for base 10.",
"tags": [
"math"
@ -136574,7 +136630,7 @@
},
{
"name": "log10",
"summary": "Computes the base 10 logarithm of the number.",
"summary": "Compute the base 10 logarithm of the number.",
"description": "",
"tags": [
"math"
@ -136607,7 +136663,7 @@
},
{
"name": "log2",
"summary": "Computes the base 2 logarithm of the number.",
"summary": "Compute the base 2 logarithm of the number.",
"description": "",
"tags": [
"math"
@ -136640,7 +136696,7 @@
},
{
"name": "max",
"summary": "Computes the maximum of the given arguments.",
"summary": "Compute the maximum of the given arguments.",
"description": "",
"tags": [
"math"
@ -136676,7 +136732,7 @@
},
{
"name": "min",
"summary": "Computes the minimum of the given arguments.",
"summary": "Compute the minimum of the given arguments.",
"description": "",
"tags": [
"math"
@ -136712,8 +136768,8 @@
},
{
"name": "patternCircular2d",
"summary": "A circular pattern on a 2D sketch.",
"description": "",
"summary": "Repeat a 2-dimensional sketch some number of times along a partial or",
"description": "complete circle some specified number of times. Each object may additionally be rotated along the circle, ensuring orentation of the solid with respect to the center of the circle is maintained.",
"tags": [],
"args": [
{
@ -144176,8 +144232,8 @@
},
{
"name": "patternCircular3d",
"summary": "A circular pattern on a 3D model.",
"description": "",
"summary": "Repeat a 3-dimensional solid some number of times along a partial or",
"description": "complete circle some specified number of times. Each object may additionally be rotated along the circle, ensuring orentation of the solid with respect to the center of the circle is maintained.",
"tags": [],
"args": [
{
@ -150924,8 +150980,8 @@
},
{
"name": "patternLinear2d",
"summary": "A linear pattern on a 2D sketch.",
"description": "",
"summary": "Repeat a 2-dimensional sketch along some dimension, with a dynamic amount",
"description": "of distance between each repetition, some specified number of times.",
"tags": [],
"args": [
{
@ -158383,8 +158439,8 @@
},
{
"name": "patternLinear3d",
"summary": "A linear pattern on a 3D model.",
"description": "",
"summary": "Repeat a 3-dimensional solid along a linear path, with a dynamic amount",
"description": "of distance between each repetition, some specified number of times.",
"tags": [],
"args": [
{
@ -165115,8 +165171,8 @@
},
{
"name": "patternTransform",
"summary": "A linear pattern on a 3D solid.",
"description": "Each repetition of the pattern can be transformed (e.g. scaled, translated, hidden, etc).",
"summary": "Repeat a 3-dimensional solid by successively applying a transformation (such",
"description": "as rotation, scale, translation, visibility) on each repetition.",
"tags": [],
"args": [
{
@ -171851,8 +171907,8 @@
},
{
"name": "polar",
"summary": "Convert from polar/sphere coordinates to cartesian coordinates.",
"description": "",
"summary": "Convert polar/sphere (azimuth, elevation, distance) coordinates to",
"description": "cartesian (x/y/z grid) coordinates.",
"tags": [],
"args": [
{
@ -171903,7 +171959,7 @@
},
{
"name": "pow",
"summary": "Computes the number to a power.",
"summary": "Compute the number to a power.",
"description": "",
"tags": [
"math"
@ -171945,8 +172001,8 @@
},
{
"name": "profileStart",
"summary": "",
"description": "",
"summary": "Extract the provided 2-dimensional sketch group's profile's origin",
"description": "value.",
"tags": [],
"args": [
{
@ -175258,8 +175314,8 @@
},
{
"name": "profileStartX",
"summary": "",
"description": "",
"summary": "Extract the provided 2-dimensional sketch group's profile's origin's 'x'",
"description": "value.",
"tags": [],
"args": [
{
@ -178566,8 +178622,8 @@
},
{
"name": "profileStartY",
"summary": "",
"description": "",
"summary": "Extract the provided 2-dimensional sketch group's profile's origin's 'y'",
"description": "value.",
"tags": [],
"args": [
{
@ -181874,8 +181930,8 @@
},
{
"name": "revolve",
"summary": "Revolve a sketch around an axis.",
"description": "",
"summary": "Rotate a sketch around some provided axis, creating a solid from its extent.",
"description": "This, like extrude, is able to create a 3-dimensional solid from a 2-dimensional sketch. However, unlike extrude, this creates a solid by using the extent of the sketch as its revolved around an axis rather than using the extent of the sketch linearly translated through a third dimension.",
"tags": [],
"args": [
{
@ -188316,7 +188372,7 @@
},
{
"name": "segAng",
"summary": "Returns the angle of the segment.",
"summary": "Compute the angle (in degrees) of the provided line segment.",
"description": "",
"tags": [],
"args": [
@ -188799,7 +188855,7 @@
},
{
"name": "segEndX",
"summary": "Returns the segment end of x.",
"summary": "Compute the ending point of the provided line segment along the 'x' axis.",
"description": "",
"tags": [],
"args": [
@ -189282,7 +189338,7 @@
},
{
"name": "segEndY",
"summary": "Returns the segment end of y.",
"summary": "Compute the ending point of the provided line segment along the 'y' axis.",
"description": "",
"tags": [],
"args": [
@ -189765,7 +189821,7 @@
},
{
"name": "segLen",
"summary": "Returns the length of the segment.",
"summary": "Compute the length of the provided line segment.",
"description": "",
"tags": [],
"args": [
@ -190248,8 +190304,8 @@
},
{
"name": "shell",
"summary": "Shell a solid.",
"description": "",
"summary": "Remove volume from a 3-dimensional shape such that a wall of the",
"description": "provided thickness remains, taking volume starting at the provided face, leaving it open in that direction.",
"tags": [],
"args": [
{
@ -195883,7 +195939,7 @@
},
{
"name": "sin",
"summary": "Computes the sine of a number (in radians).",
"summary": "Compute the sine of a number (in radians).",
"description": "",
"tags": [
"math"
@ -195916,7 +195972,7 @@
},
{
"name": "sqrt",
"summary": "Computes the square root of a number.",
"summary": "Compute the square root of a number.",
"description": "",
"tags": [
"math"
@ -195949,7 +196005,7 @@
},
{
"name": "startProfileAt",
"summary": "Start a profile at a given point.",
"summary": "Start a new profile at a given point.",
"description": "",
"tags": [],
"args": [
@ -201642,7 +201698,7 @@
},
{
"name": "startSketchAt",
"summary": "Start a sketch at a given point on the 'XY' plane.",
"summary": "Start a new 2-dimensional sketch at a given point on the 'XY' plane.",
"description": "",
"tags": [],
"args": [
@ -204957,7 +205013,7 @@
},
{
"name": "startSketchOn",
"summary": "Start a sketch on a specific plane or face.",
"summary": "Start a new 2-dimensional sketch on a specific plane or face.",
"description": "",
"tags": [],
"args": [
@ -210010,7 +210066,7 @@
},
{
"name": "tan",
"summary": "Computes the tangent of a number (in radians).",
"summary": "Compute the tangent of a number (in radians).",
"description": "",
"tags": [
"math"
@ -210043,8 +210099,8 @@
},
{
"name": "tangentialArc",
"summary": "Draw an arc.",
"description": "",
"summary": "Starting at the current sketch's origin, draw a curved line segment along",
"description": "some part of an imaginary circle of the specified radius.\nThe arc is constructed such that the last line segment is placed tangent to the imaginary circle of the specified radius. The resulting arc is the segment of the imaginary circle from that tangent point for 'offset' degrees along the imaginary circle.",
"tags": [],
"args": [
{
@ -216707,8 +216763,8 @@
},
{
"name": "tangentialArcTo",
"summary": "Draw an arc.",
"description": "",
"summary": "Starting at the current sketch's origin, draw a curved line segment along",
"description": "some part of an imaginary circle until it reaches the desired (x, y) coordinates.",
"tags": [],
"args": [
{
@ -223435,8 +223491,8 @@
},
{
"name": "xLine",
"summary": "Draw a line on the x-axis.",
"description": "",
"summary": "Draw a line relative to the current origin to a specified distance away",
"description": "from the current position along the 'x' axis.",
"tags": [],
"args": [
{
@ -230069,8 +230125,8 @@
},
{
"name": "xLineTo",
"summary": "Draw a line to a point on the x-axis.",
"description": "",
"summary": "Draw a line parallel to the X axis, that ends at the given X.",
"description": "E.g. if the previous line ended at (1, 1), then xLineTo(4) draws a line from (1, 1) to (4, 1)",
"tags": [],
"args": [
{
@ -236703,8 +236759,8 @@
},
{
"name": "yLine",
"summary": "Draw a line on the y-axis.",
"description": "",
"summary": "Draw a line relative to the current origin to a specified distance away",
"description": "from the current position along the 'y' axis.",
"tags": [],
"args": [
{
@ -243337,8 +243393,8 @@
},
{
"name": "yLineTo",
"summary": "Draw a line to a point on the y-axis.",
"description": "",
"summary": "Draw a line parallel to the Y axis, that ends at the given Y.",
"description": "E.g. if the previous line ended at (1, 1), then yLineTo(4) draws a line from (1, 1) to (1, 4)",
"tags": [],
"args": [
{

View File

@ -1,10 +1,10 @@
---
title: "tan"
excerpt: "Computes the tangent of a number (in radians)."
excerpt: "Compute the tangent of a number (in radians)."
layout: manual
---
Computes the tangent of a number (in radians).
Compute the tangent of a number (in radians).

View File

@ -1,12 +1,13 @@
---
title: "tangentialArc"
excerpt: "Draw an arc."
excerpt: "Starting at the current sketch's origin, draw a curved line segment along"
layout: manual
---
Draw an arc.
Starting at the current sketch's origin, draw a curved line segment along
some part of an imaginary circle of the specified radius.
The arc is constructed such that the last line segment is placed tangent to the imaginary circle of the specified radius. The resulting arc is the segment of the imaginary circle from that tangent point for 'offset' degrees along the imaginary circle.
```js
tangentialArc(data: TangentialArcData, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,12 +1,12 @@
---
title: "tangentialArcTo"
excerpt: "Draw an arc."
excerpt: "Starting at the current sketch's origin, draw a curved line segment along"
layout: manual
---
Draw an arc.
Starting at the current sketch's origin, draw a curved line segment along
some part of an imaginary circle until it reaches the desired (x, y) coordinates.
```js
tangentialArcTo(to: [number], sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,12 +1,12 @@
---
title: "xLine"
excerpt: "Draw a line on the x-axis."
excerpt: "Draw a line relative to the current origin to a specified distance away"
layout: manual
---
Draw a line on the x-axis.
Draw a line relative to the current origin to a specified distance away
from the current position along the 'x' axis.
```js
xLine(length: number, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,12 +1,12 @@
---
title: "xLineTo"
excerpt: "Draw a line to a point on the x-axis."
excerpt: "Draw a line parallel to the X axis, that ends at the given X."
layout: manual
---
Draw a line to a point on the x-axis.
Draw a line parallel to the X axis, that ends at the given X.
E.g. if the previous line ended at (1, 1), then xLineTo(4) draws a line from (1, 1) to (4, 1)
```js
xLineTo(to: number, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,12 +1,12 @@
---
title: "yLine"
excerpt: "Draw a line on the y-axis."
excerpt: "Draw a line relative to the current origin to a specified distance away"
layout: manual
---
Draw a line on the y-axis.
Draw a line relative to the current origin to a specified distance away
from the current position along the 'y' axis.
```js
yLine(length: number, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -1,12 +1,12 @@
---
title: "yLineTo"
excerpt: "Draw a line to a point on the y-axis."
excerpt: "Draw a line parallel to the Y axis, that ends at the given Y."
layout: manual
---
Draw a line to a point on the y-axis.
Draw a line parallel to the Y axis, that ends at the given Y.
E.g. if the previous line ended at (1, 1), then yLineTo(4) draws a line from (1, 1) to (1, 4)
```js
yLineTo(to: number, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup

View File

@ -0,0 +1,153 @@
import { test, expect, Page } from '@playwright/test'
import {
getUtils,
TEST_COLORS,
setup,
tearDown,
commonPoints,
PERSIST_MODELING_CONTEXT,
} from './test-utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.setTimeout(120000)
async function doBasicSketch(page: Page, openPanes: string[]) {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
// If we have the code pane open, we should see the code.
if (openPanes.includes('code')) {
await expect(u.codeLocator).toHaveText(``)
} else {
// Ensure we don't see the code.
await expect(u.codeLocator).not.toBeVisible()
}
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// select a plane
await page.mouse.click(700, 200)
if (openPanes.includes('code')) {
await expect(u.codeLocator).toHaveText(
`const sketch001 = startSketchOn('XZ')`
)
}
await u.closeDebugPanel()
await page.waitForTimeout(1000) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
if (openPanes.includes('code')) {
await expect(u.codeLocator)
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)`)
}
await page.waitForTimeout(500)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(500)
if (openPanes.includes('code')) {
await expect(u.codeLocator)
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
}
await page.waitForTimeout(500)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
if (openPanes.includes('code')) {
await expect(u.codeLocator)
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 + 0.01}], %)`)
} else {
await page.waitForTimeout(500)
}
await page.mouse.click(startXPx, 500 - PUR * 20)
if (openPanes.includes('code')) {
await expect(u.codeLocator)
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1 + 0.01}], %)
|> line([-${commonPoints.num2}, 0], %)`)
}
// deselect line tool
await page.getByRole('button', { name: 'Line', exact: true }).click()
await page.waitForTimeout(500)
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
if (openPanes.includes('code')) {
expect(await u.getGreatestPixDiff(line1, TEST_COLORS.WHITE)).toBeLessThan(3)
await expect(
await u.getGreatestPixDiff(line1, [249, 249, 249])
).toBeLessThan(3)
}
// click between first two clicks to get center of the line
await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
expect(await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)).toBeLessThan(3)
await expect(await u.getGreatestPixDiff(line1, [0, 0, 255])).toBeLessThan(3)
}
// hold down shift
await page.keyboard.down('Shift')
// click between the latest two clicks to get center of the line
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20)
// selected two lines therefore there should be two cursors
if (openPanes.includes('code')) {
await expect(page.locator('.cm-cursor')).toHaveCount(2)
}
await page.getByRole('button', { name: 'Length: open menu' }).click()
await page.getByRole('button', { name: 'Equal Length' }).click()
// Open the code pane.
await u.openKclCodePanel()
await expect(u.codeLocator).toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %, $seg01)
|> line([0, ${commonPoints.num1 + 0.01}], %)
|> angledLine([180, segLen(seg01)], %)`)
}
test.describe('Basic sketch', () => {
test('code pane open at start', async ({ page }) => {
await doBasicSketch(page, ['code'])
})
test('code pane closed at start', async ({ page }) => {
// Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem(
persistModelingContext,
JSON.stringify({ openPanes: [] })
)
}, PERSIST_MODELING_CONTEXT)
await doBasicSketch(page, [])
})
})

View File

@ -0,0 +1,111 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Can create sketches on all planes and their back sides', () => {
const sketchOnPlaneAndBackSideTest = async (
page: any,
plane: string,
clickCoords: { x: number; y: number }
) => {
const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
const coord =
plane === '-XY' || plane === '-YZ' || plane === 'XZ' ? -100 : 100
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: coord, y: coord, z: coord },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
const code = `const sketch001 = startSketchOn('${plane}')
|> startProfileAt([0.9, -1.22], %)`
await u.openDebugPanel()
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await u.closeDebugPanel()
await page.mouse.click(clickCoords.x, clickCoords.y)
await page.waitForTimeout(300) // wait for animation
await expect(
page.getByRole('button', { name: 'Line', exact: true })
).toBeVisible()
// draw a line
const startXPx = 600
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')).toHaveText(code)
await page.getByRole('button', { name: 'Line', exact: true }).click()
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearCommandLogs()
await u.removeCurrentCode()
}
test('XY', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(
page,
'XY',
{ x: 600, y: 388 } // red plane
// { x: 600, y: 400 }, // red plane // clicks grid helper and that causes problems, should fix so that these coords work too.
)
})
test('YZ', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, 'YZ', { x: 700, y: 250 }) // green plane
})
test('XZ', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, '-XZ', { x: 700, y: 80 }) // blue plane
})
test('-XY', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, '-XY', { x: 600, y: 118 }) // back of red plane
})
test('-YZ', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, '-YZ', { x: 700, y: 219 }) // back of green plane
})
test('-XZ', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, 'XZ', { x: 700, y: 427 }) // back of blue plane
})
})

View File

@ -0,0 +1,219 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import { bracket } from 'lib/exampleKcl'
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Code pane and errors', () => {
test('Typing KCL errors induces a badge on the code pane button', async ({
page,
}) => {
const u = await getUtils(page)
// Load the app with the working starter code
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, bracket)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// Ensure no badge is present
const codePaneButtonHolder = page.locator('#code-button-holder')
await expect(codePaneButtonHolder).not.toContainText('notification')
// Delete a character to break the KCL
await u.openKclCodePanel()
await page.getByText('extrude(').click()
await page.keyboard.press('Backspace')
// Ensure that a badge appears on the button
await expect(codePaneButtonHolder).toContainText('notification')
})
test('Opening and closing the code pane will consistently show error diagnostics', async ({
page,
}) => {
const u = await getUtils(page)
// Load the app with the working starter code
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, bracket)
await page.setViewportSize({ width: 1200, height: 900 })
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// Ensure we have no errors in the gutter.
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Ensure no badge is present
const codePaneButton = page.getByRole('button', { name: 'KCL Code pane' })
const codePaneButtonHolder = page.locator('#code-button-holder')
await expect(codePaneButtonHolder).not.toContainText('notification')
// Delete a character to break the KCL
await u.openKclCodePanel()
await page.getByText('extrude(').click()
await page.keyboard.press('Backspace')
// Ensure that a badge appears on the button
await expect(codePaneButtonHolder).toContainText('notification')
// Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token').first()).toBeVisible()
// Close the code pane
await codePaneButton.click()
await page.waitForTimeout(500)
// Ensure that a badge appears on the button
await expect(codePaneButtonHolder).toContainText('notification')
// Ensure we have no errors in the gutter.
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Open the code pane
await u.openKclCodePanel()
// Ensure that a badge appears on the button
await expect(codePaneButtonHolder).toContainText('notification')
// Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token').first()).toBeVisible()
})
test('When error is not in view you can click the badge to scroll to it', async ({
page,
}) => {
const u = await getUtils(page)
// Load the app with the working starter code
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.waitForTimeout(1000)
// Ensure badge is present
const codePaneButtonHolder = page.locator('#code-button-holder')
await expect(codePaneButtonHolder).toContainText('notification')
// Ensure we have no errors in the gutter, since error out of view.
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Click the badge.
const badge = page.locator('#code-badge')
await expect(badge).toBeVisible()
await badge.click()
// Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
// Hover over the error to see the error message
await page.hover('.cm-lint-marker-error')
await expect(
page
.getByText(
'sketch profile must lie entirely on one side of the revolution axis'
)
.first()
).toBeVisible()
})
test('When error is not in view WITH LINTS you can click the badge to scroll to it', async ({
page,
}) => {
const u = await getUtils(page)
// Load the app with the working starter code
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.waitForTimeout(1000)
// Ensure badge is present
const codePaneButtonHolder = page.locator('#code-button-holder')
await expect(codePaneButtonHolder).toContainText('notification')
// Ensure we have no errors in the gutter, since error out of view.
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// click in the editor to focus it
await page.locator('.cm-content').click()
await page.waitForTimeout(500)
// go to the start of the editor and enter more text which will trigger
// a lint error.
// GO to the start of the editor.
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('Home')
await page.keyboard.type('const foo_bar = 1')
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
// ensure we have a lint error
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// Click the badge.
const badge = page.locator('#code-badge')
await expect(badge).toBeVisible()
await badge.click()
// Ensure we have an error diagnostic.
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
// Hover over the error to see the error message
await page.hover('.cm-lint-marker-error')
await expect(
page
.getByText(
'sketch profile must lie entirely on one side of the revolution axis'
)
.first()
).toBeVisible()
})
})

View File

@ -0,0 +1,363 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Command bar tests', () => {
test('Extrude from command bar selects extrude line after', async ({
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> xLine(-20, %)
|> close(%)
`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// Click the line of code for xLine.
await page.getByText(`close(%)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Extrude' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toHaveText(
`const extrude001 = extrude(${KCL_DEFAULT_LENGTH}, sketch001)`
)
})
test('Fillet from command bar', async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XY')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-10, sketch001)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([0, -10], %)`).click()
await selectSegment()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Fillet' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toContainText(
`fillet({ radius: ${KCL_DEFAULT_LENGTH}, tags: [seg01] }, %)`
)
})
test('Command bar can change a setting, and switch back and forth between arguments', async ({
page,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
const commandBarButton = page.getByRole('button', { name: 'Commands' })
const cmdSearchBar = page.getByPlaceholder('Search commands')
const themeOption = page.getByRole('option', {
name: 'theme',
exact: false,
})
const commandLevelArgButton = page.getByRole('button', { name: 'level' })
const commandThemeArgButton = page.getByRole('button', { name: 'value' })
// This selector changes after we set the setting
let commandOptionInput = page.getByPlaceholder('Select an option')
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
// First try opening the command bar and closing it
await page
.getByRole('button', { name: 'Commands', exact: false })
.or(page.getByRole('button', { name: '⌘K' }))
.click()
await expect(cmdSearchBar).toBeVisible()
await page.keyboard.press('Escape')
await expect(cmdSearchBar).not.toBeVisible()
// Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K')
await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar
await cmdSearchBar.fill('theme')
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option')
await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused()
// Select dark theme
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set theme to "system" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
commandOptionInput = page.getByPlaceholder('system')
// Test case for https://github.com/KittyCAD/modeling-app/issues/2882
await commandBarButton.click()
await cmdSearchBar.focus()
await cmdSearchBar.fill('theme')
await themeOption.click()
await expect(commandThemeArgButton).toBeDisabled()
await commandOptionInput.focus()
await commandOptionInput.fill('lig')
await commandLevelArgButton.click()
await expect(commandLevelArgButton).toBeDisabled()
// Test case for https://github.com/KittyCAD/modeling-app/issues/2881
await commandThemeArgButton.click()
await expect(commandThemeArgButton).toBeDisabled()
await expect(commandLevelArgButton).toHaveText('level: project')
})
test('Command bar keybinding works from code editor and can change a setting', async ({
page,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
// Put the cursor in the code editor
await page.locator('.cm-content').click()
// Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K')
let cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar
await cmdSearchBar.fill('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('Select an option')
await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused()
// Select dark theme
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set theme to "system" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
})
test('Can extrude from the command bar', async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const distance = sqrt(20)
const sketch001 = startSketchOn('XZ')
|> startProfileAt([-6.95, 10.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -20.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// Make sure the stream is up
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Extrude' }).isEnabled()
let cmdSearchBar = page.getByPlaceholder('Search commands')
await page.keyboard.press('Meta+K')
await expect(cmdSearchBar).toBeVisible()
// Search for extrude command and choose it
await page.getByRole('option', { name: 'Extrude' }).click()
// Assert that we're on the selection step
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
// Select a face
await page.mouse.move(700, 200)
await page.mouse.click(700, 200)
// Assert that we're on the distance step
await expect(
page.getByRole('button', { name: 'distance', exact: false })
).toBeDisabled()
// Assert that the an alternative variable name is chosen,
// since the default variable name is already in use (distance)
await page.getByRole('button', { name: 'Create new variable' }).click()
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'distance001'
)
const continueButton = page.getByRole('button', { name: 'Continue' })
const submitButton = page.getByRole('button', { name: 'Submit command' })
await continueButton.click()
// Review step and argument hotkeys
await expect(submitButton).toBeEnabled()
await expect(submitButton).toBeFocused()
await submitButton.press('Backspace')
// Assert we're back on the distance step
await expect(
page.getByRole('button', { name: 'distance', exact: false })
).toBeDisabled()
await continueButton.click()
await submitButton.click()
// Check that the code was updated
await u.waitForCmdReceive('extrude')
// Unfortunately this indentation seems to matter for the test
await expect(page.locator('.cm-content')).toHaveText(
`const distance = sqrt(20)
const distance001 = ${KCL_DEFAULT_LENGTH}
const sketch001 = startSketchOn('XZ')
|> startProfileAt([-6.95, 10.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -20.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
const extrude001 = extrude(distance001, sketch001)`.replace(
/(\r\n|\n|\r)/gm,
''
) // remove newlines
)
})
test('Can switch between sketch tools via command bar', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
const sketchButton = page.getByRole('button', { name: 'Start Sketch' })
const cmdBarButton = page.getByRole('button', { name: 'Commands' })
const rectangleToolCommand = page.getByRole('option', {
name: 'rectangle',
})
const rectangleToolButton = page.getByRole('button', {
name: 'Corner rectangle',
exact: true,
})
const lineToolCommand = page.getByRole('option', {
name: 'Line',
})
const lineToolButton = page.getByRole('button', {
name: 'Line',
exact: true,
})
const arcToolCommand = page.getByRole('option', { name: 'Tangential Arc' })
const arcToolButton = page.getByRole('button', {
name: 'Tangential Arc',
exact: true,
})
// Start a sketch
await sketchButton.click()
await page.mouse.click(700, 200)
// Switch between sketch tools via the command bar
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
await cmdBarButton.click()
await rectangleToolCommand.click()
await expect(rectangleToolButton).toHaveAttribute('aria-pressed', 'true')
await cmdBarButton.click()
await lineToolCommand.click()
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')
// Click in the scene a couple times to draw a line
// so tangential arc is valid
await page.mouse.click(700, 200)
await page.mouse.move(700, 300, { steps: 5 })
await page.mouse.click(700, 300)
// switch to tangential arc via command bar
await cmdBarButton.click()
await arcToolCommand.click()
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
})
})

View File

@ -0,0 +1,519 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Copilot ghost text', () => {
test.skip(true, 'Needs to get covered again')
test('completes code in empty file', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// We should be able to hit Tab to accept the completion.
await page.keyboard.press('Tab')
await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
// Hit enter a few times.
await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %) `
)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
})
test.skip('copilot disabled in sketch mode no select plane', async ({
page,
}) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
// Click sketch mode.
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.codeLocator.click()
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await page.waitForTimeout(500)
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``)
// Exit sketch mode.
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(500)
await u.codeLocator.click()
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// We should be able to hit Tab to accept the completion.
await page.keyboard.press('Tab')
await expect(page.locator('.cm-content')).toContainText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
})
test('copilot disabled in sketch mode after selecting plane', async ({
page,
}) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
// Click sketch mode.
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click()
// select a plane
await page.mouse.click(700, 200)
await page.waitForTimeout(700) // wait for animation
await u.codeLocator.click()
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await page.waitForTimeout(500)
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')`
)
// Escape to exit the tool.
await u.openDebugPanel()
await u.closeDebugPanel()
await page.keyboard.press('Escape')
await page.waitForTimeout(500)
await u.codeLocator.click()
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await page.waitForTimeout(500)
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')`
)
// Escape again to exit sketch mode.
await u.openDebugPanel()
await u.closeDebugPanel()
await page.keyboard.press('Escape')
await page.waitForTimeout(500)
await u.codeLocator.click()
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// We should be able to hit Tab to accept the completion.
await page.keyboard.press('Tab')
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
// Hit enter a few times.
await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %) `
)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
})
test('ArrowUp in code rejects the suggestion', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// Going elsewhere in the code should hide the ghost text.
await page.keyboard.press('ArrowUp')
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``)
})
test('ArrowDown in code rejects the suggestion', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// Going elsewhere in the code should hide the ghost text.
await page.keyboard.press('ArrowDown')
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``)
})
test('ArrowLeft in code rejects the suggestion', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// Going elsewhere in the code should hide the ghost text.
await page.keyboard.press('ArrowLeft')
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``)
})
test('ArrowRight in code rejects the suggestion', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// Going elsewhere in the code should hide the ghost text.
await page.keyboard.press('ArrowRight')
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``)
})
test('Enter in code scoots it down', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// Going elsewhere in the code should hide the ghost text.
await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
})
test('Ctrl+shift+z in code rejects the suggestion', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control'
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// Going elsewhere in the code should hide the ghost text.
await page.keyboard.down(CtrlKey)
await page.keyboard.down('Shift')
await page.keyboard.press('KeyZ')
await page.keyboard.up(CtrlKey)
await page.keyboard.up('Shift')
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``)
})
test('Ctrl+z in code rejects the suggestion and undos the last code', async ({
page,
}) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control'
await page.waitForTimeout(800)
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
await page.keyboard.type('{thing: "blah"}', { delay: 0 })
await expect(page.locator('.cm-content')).toHaveText(`{thing: "blah"}`)
// We wanna make sure the code saves.
await page.waitForTimeout(800)
// Ctrl+z
await page.keyboard.down(CtrlKey)
await page.keyboard.press('KeyZ')
await page.keyboard.up(CtrlKey)
await expect(page.locator('.cm-content')).toHaveText(``)
// Ctrl+shift+z
await page.keyboard.down(CtrlKey)
await page.keyboard.down('Shift')
await page.keyboard.press('KeyZ')
await page.keyboard.up(CtrlKey)
await page.keyboard.up('Shift')
await expect(page.locator('.cm-content')).toHaveText(`{thing: "blah"}`)
// We wanna make sure the code saves.
await page.waitForTimeout(800)
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(
`{thing: "blah"}fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// Once for the enter.
await page.keyboard.down(CtrlKey)
await page.keyboard.press('KeyZ')
await page.keyboard.up(CtrlKey)
// Once for the text.
await page.keyboard.down(CtrlKey)
await page.keyboard.press('KeyZ')
await page.keyboard.up(CtrlKey)
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
// TODO when we make codemirror a widget, we can test this.
//await expect(page.locator('.cm-content')).toHaveText(``)
})
test('delete in code rejects the suggestion', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
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-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// Going elsewhere in the code should hide the ghost text.
await page.keyboard.press('Delete')
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``)
})
test('backspace in code rejects the suggestion', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
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-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// Going elsewhere in the code should hide the ghost text.
await page.keyboard.press('Backspace')
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``)
})
test('focus outside code pane rejects the suggestion', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
)
await expect(page.locator('.cm-ghostText').first()).toHaveText(
`fn cube = (pos, scale) => {`
)
// Going outside the editor should hide the ghost text.
await page.mouse.move(0, 0)
await page
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``)
})
})

View File

@ -0,0 +1,867 @@
import { test, expect } from '@playwright/test'
import { uuidv4 } from 'lib/utils'
import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
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()
const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control'
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.codeLocator.click()
await page.keyboard.type(`const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
await page.keyboard.down(CtrlKey)
await page.keyboard.press('/')
await page.keyboard.up(CtrlKey)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
// |> close(%)`)
// uncomment the code
await page.keyboard.down(CtrlKey)
await page.keyboard.press('/')
await page.keyboard.up(CtrlKey)
await expect(page.locator('.cm-content'))
.toHaveText(`const 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', async ({
page,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.codeLocator.click()
await page.keyboard.type(`const 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(`const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
})
test('fold gutters work', async ({ page }) => {
const u = await getUtils(page)
const fullCode = `const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// TODO: Jess needs to fix this but you have to mod the code to get them to show
// up, its an annoying codemirror thing.
await page.locator('.cm-content').click()
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter')
const foldGutterFoldLine = page.locator('[title="Fold line"]')
const foldGutterUnfoldLine = page.locator('[title="Unfold line"]')
await expect(page.locator('.cm-content')).toHaveText(fullCode)
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Make sure we have a fold gutter
await expect(foldGutterFoldLine).toBeVisible()
await expect(foldGutterUnfoldLine).not.toBeVisible()
// Collapse the code
await foldGutterFoldLine.click()
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XY')… `
)
await expect(page.locator('.cm-content')).not.toHaveText(fullCode)
await expect(foldGutterFoldLine).not.toBeVisible()
await expect(foldGutterUnfoldLine.nth(1)).toBeVisible()
// Expand the code
await foldGutterUnfoldLine.nth(1).click()
await expect(page.locator('.cm-content')).toHaveText(fullCode)
// Delete all the code.
await page.locator('.cm-content').click()
// Select all
await page.keyboard.press('Control+A')
await page.keyboard.press('Backspace')
await page.keyboard.press('Meta+A')
await page.keyboard.press('Backspace')
await expect(page.locator('.cm-content')).toHaveText(``)
await expect(page.locator('.cm-content')).not.toHaveText(fullCode)
await expect(foldGutterUnfoldLine).not.toBeVisible()
await expect(foldGutterFoldLine).not.toBeVisible()
})
test('hover over functions shows function description', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// focus the editor
await u.codeLocator.click()
// Hover over the startSketchOn function
await page.getByText('startSketchOn').hover()
await expect(page.locator('.hover-tooltip')).toBeVisible()
await expect(
page.getByText(
'Start a new 2-dimensional sketch on a specific plane or face'
)
).toBeVisible()
// Hover over the line function
await page.getByText('line').first().hover()
await expect(page.locator('.hover-tooltip')).toBeVisible()
await expect(page.getByText('Draw a line')).toBeVisible()
})
test('if you use the format keyboard binding it formats your code', async ({
page,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`
)
localStorage.setItem('disableAxis', 'true')
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// focus the editor
await u.codeLocator.click()
// Hit alt+shift+f to format the code
await page.keyboard.press('Alt+Shift+KeyF')
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
})
test('if you write kcl with lint errors you get lints', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible()
await u.codeLocator.click()
await page.keyboard.type('const my_snake_case_var = 5')
await page.keyboard.press('Enter')
await page.keyboard.type('const myCamelCaseVar = 5')
await page.keyboard.press('Enter')
// press arrows to clear autocomplete
await page.keyboard.press('ArrowLeft')
await page.keyboard.press('ArrowRight')
// error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-info')
await expect(
page.getByText('Identifiers must be lowerCamelCase').first()
).toBeVisible()
// select the line that's causing the error and delete it
await page.getByText('const my_snake_case_var = 5').click()
await page.keyboard.press('End')
await page.keyboard.down('Shift')
await page.keyboard.press('Home')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
// wait for .cm-lint-marker-info not to be visible
await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible()
})
test('if you fixup kcl errors you clear lints', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([3.29, 7.86], %)
|> line([2.48, 2.44], %)
|> line([2.66, 1.17], %)
|> close(%)
`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.codeLocator.click()
await page.getByText(' |> line([2.48, 2.44], %)').click()
await expect(
page.locator('.cm-lint-marker-error').first()
).not.toBeVisible()
await page.keyboard.press('End')
await page.keyboard.press('Backspace')
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
await page.keyboard.type(')')
await expect(
page.locator('.cm-lint-marker-error').first()
).not.toBeVisible()
})
test('if you write invalid kcl you get inlined errors', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
/* add the following code to the editor ($ error is not a valid line)
$ error
const topAng = 30
const bottomAng = 25
*/
await u.codeLocator.click()
await page.keyboard.type('$ error')
// press arrows to clear autocomplete
await page.keyboard.press('ArrowLeft')
await page.keyboard.press('ArrowRight')
await page.keyboard.press('Enter')
await page.keyboard.type('const topAng = 30')
await page.keyboard.press('Enter')
await page.keyboard.type('const bottomAng = 25')
await page.keyboard.press('Enter')
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token').first()).toBeVisible()
// select the line that's causing the error and delete it
await page.getByText('$ error').click()
await page.keyboard.press('End')
await page.keyboard.down('Shift')
await page.keyboard.press('Home')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
// wait for .cm-lint-marker-error not to be visible
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// let's check we get an error when defining the same variable twice
await page.getByText('const bottomAng = 25').click()
await page.keyboard.press('Enter')
await page.keyboard.type("// Let's define the same thing twice")
await page.keyboard.press('Enter')
await page.keyboard.type('const topAng = 42')
await page.keyboard.press('ArrowLeft')
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
await expect(
page.locator('.cm-lint-marker.cm-lint-marker-error')
).toBeVisible()
await page.locator('.cm-lint-marker.cm-lint-marker-error').hover()
await expect(page.locator('.cm-diagnosticText').first()).toBeVisible()
await expect(
page.getByText('Cannot redefine `topAng`').first()
).toBeVisible()
const secondTopAng = page.getByText('topAng').first()
await secondTopAng?.dblclick()
await page.keyboard.type('otherAng')
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
})
test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const length = .750
const width = 0.500
const height = 0.500
const dia = 4
fn squareHole = (l, w) => {
const squareHoleSketch = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %)
|> lineTo([width / 2, -length / 2], %)
|> lineTo([width / 2, length / 2], %)
|> lineTo([-width / 2, length / 2], %)
|> close(%)
return squareHoleSketch
}
`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Click on the bottom of the code editor to add a new line
await u.codeLocator.click()
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter')
await page.keyboard.type(`const extrusion = startSketchOn('XY')
|> circle([0, 0], dia/2, %)
|> hole(squareHole(length, width, height), %)
|> extrude(height, %)`)
// error in gutter
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
await page.hover('.cm-lint-marker-error:first-child')
await expect(
page.getByText('Expected 2 arguments, got 3').first()
).toBeVisible()
// Make sure there are two diagnostics
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2)
})
test('if your kcl gets an error from the engine it is inlined', async ({
page,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const box = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %, $revolveAxis)
|> close(%)
|> extrude(10, %)
const sketch001 = startSketchOn(box, revolveAxis)
|> startProfileAt([5, 10], %)
|> line([0, -10], %)
|> line([2, 0], %)
|> line([0, -10], %)
|> close(%)
|> revolve({
axis: revolveAxis,
angle: 90
}, %)
`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForPageLoad()
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
const searchText =
'sketch profile must lie entirely on one side of the revolution axis'
await expect(page.getByText(searchText)).toBeVisible()
})
test.describe('Autocomplete works', () => {
test('with enter/click to accept the completion', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// this test might be brittle as we add and remove functions
// but should also be easy to update.
// tests clicking on an option, selection the first option
// and arrowing down to an option
await u.codeLocator.click()
await page.keyboard.type('const sketch001 = start')
// expect there to be six auto complete options
await expect(page.locator('.cm-completionLabel')).toHaveCount(8)
// this makes sure we can accept a completion with click
await page.getByText('startSketchOn').click()
await page.keyboard.type("'XZ'")
await page.keyboard.press('Tab')
await page.keyboard.press('Enter')
await page.keyboard.type(' |> startProfi')
// expect there be a single auto complete option that we can just hit enter on
await expect(page.locator('.cm-completionLabel')).toBeVisible()
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // accepting the auto complete, not a new line
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.type('12')
await page.waitForTimeout(100)
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.type(' |> lin')
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible()
await page.waitForTimeout(100)
// press arrow down twice then enter to accept xLine
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter')
// finish line with comment
await page.keyboard.type('5')
await page.waitForTimeout(100)
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Tab')
await page.keyboard.type(' // ')
// Since we need to parse the ast to know we are in a comment we gotta hang tight.
await page.waitForTimeout(700)
await page.keyboard.type('lin ')
await page.waitForTimeout(200)
// there shouldn't be any auto complete options for 'lin' in the comment
await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([3.14, 12], %)
|> xLine(5, %) // lin`)
})
test('with tab to accept the completion', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// this test might be brittle as we add and remove functions
// but should also be easy to update.
// tests clicking on an option, selection the first option
// and arrowing down to an option
await u.codeLocator.click()
await page.keyboard.type('const sketch001 = startSketchO')
await page.waitForTimeout(100)
// Make sure just hitting tab will take the only one left
await expect(page.locator('.cm-completionLabel')).toHaveCount(1)
await page.waitForTimeout(500)
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Tab')
await page.waitForTimeout(500)
await page.keyboard.type("'XZ'")
await page.keyboard.press('Tab')
await page.keyboard.press('Enter')
await page.keyboard.type(' |> startProfi')
// expect there be a single auto complete option that we can just hit enter on
await expect(page.locator('.cm-completionLabel')).toBeVisible()
await page.waitForTimeout(100)
await page.keyboard.press('Tab') // accepting the auto complete, not a new line
await page.keyboard.press('Tab')
await page.keyboard.type('12')
await page.waitForTimeout(100)
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.type(' |> lin')
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible()
await page.waitForTimeout(100)
// press arrow down twice then tab to accept xLine
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Tab')
// finish line with comment
await page.keyboard.type('5')
await page.waitForTimeout(100)
await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Tab')
await page.keyboard.type(' // ')
// Since we need to parse the ast to know we are in a comment we gotta hang tight.
await page.waitForTimeout(700)
await page.keyboard.type('lin ')
await page.waitForTimeout(200)
// there shouldn't be any auto complete options for 'lin' in the comment
await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([3.14, 12], %)
|> xLine(5, %) // lin`)
})
})
test('Can undo a click and point extrude with ctrl+z', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
const startPX = [665, 458]
const dragPX = 40
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeVisible()
await page.getByRole('button', { name: 'Extrude' }).click()
await expect(page.getByTestId('command-bar')).toBeVisible()
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await expect(page.getByText('Confirm Extrude')).toBeVisible()
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
// expect the code to have changed
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ') |> startProfileAt([4.61, -14.01], %) |> line([12.73, -0.09], %) |> tangentialArcTo([24.95, -5.38], %) |> close(%)const extrude001 = extrude(5, sketch001)`
)
// Now hit undo
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)`)
})
// failing for the same reason as "Can edit a sketch that has been extruded in the same pipe"
// please fix together
test.fixme('Can undo a sketch modification with ctrl+z', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)
|> extrude(5, %)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
const startPX = [665, 458]
const dragPX = 40
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText()
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
// drag startProfieAt handle
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0], y: startPX[1] },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag line handle
// we wait so it saves the code
await page.waitForTimeout(800)
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.waitForTimeout(100)
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX },
})
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// we wait so it saves the code
await page.waitForTimeout(800)
// drag tangentialArcTo handle
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: tangentEnd.x, y: tangentEnd.y - 5 },
targetPosition: {
x: tangentEnd.x + dragPX,
y: tangentEnd.y + dragPX,
},
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -16.82], %)
|> line([15.4, -2.74], %)
|> tangentialArcTo([24.95, -5.38], %)
|> line([2.65, -2.69], %)
|> close(%)
|> extrude(5, %)`)
// Hit undo
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -16.82], %)
|> line([15.4, -2.74], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)
|> extrude(5, %)`)
// Hit undo again.
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -16.82], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)
|> extrude(5, %)`)
// Hit undo again.
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)
|> extrude(5, %)`)
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,341 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import { bracket } from 'lib/exampleKcl'
import { onboardingPaths } from 'routes/Onboarding/paths'
import {
TEST_SETTINGS_KEY,
TEST_SETTINGS_ONBOARDING_START,
TEST_SETTINGS_ONBOARDING_EXPORT,
TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING,
TEST_SETTINGS_ONBOARDING_USER_MENU,
} from './storageStates'
import * as TOML from '@iarna/toml'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Onboarding tests', () => {
test('Onboarding code is shown in the editor', async ({ page }) => {
const u = await getUtils(page)
// Override beforeEach test setup
await page.addInitScript(
async ({ settingsKey }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.removeItem('persistCode')
localStorage.removeItem(settingsKey)
},
{ settingsKey: TEST_SETTINGS_KEY }
)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// 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('Code resets after confirmation', async ({ page }) => {
const initialCode = `const sketch001 = startSketchOn('XZ')`
// Load the page up with some code so we see the confirmation warning
// when we go to replay onboarding
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// Replay the onboarding
await page.getByRole('link', { name: 'Settings' }).last().click()
const replayButton = page.getByRole('button', { 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')
})
).toContain('// Shelf Bracket')
})
test('Click through each onboarding step', async ({ page }) => {
const u = await getUtils(page)
// Override beforeEach test setup
await page.addInitScript(
async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.setItem('persistCode', '')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_START }),
}
)
await page.setViewportSize({ width: 1200, height: 1080 })
await u.waitForAuthSkipAppStart()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
while ((await nextButton.innerText()) !== 'Finish') {
await expect(nextButton).toBeVisible()
await nextButton.click()
}
// Finish the onboarding
await expect(nextButton).toBeVisible()
await nextButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect(page.url()).not.toContain('onboarding')
})
test('Onboarding redirects and code updating', async ({ page }) => {
const u = await getUtils(page)
// Override beforeEach test setup
await page.addInitScript(
async ({ settingsKey, settings }) => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', 'const sigmaAllow = 15000')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_EXPORT }),
}
)
await page.setViewportSize({ width: 1200, height: 500 })
await 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)
}
})
})

View File

@ -0,0 +1,226 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Regression tests', () => {
// bugs we found that don't fit neatly into other categories
test('bad model has inline error #3251', async ({ page }) => {
// 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
// Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch2 = startSketchOn("XY")
const sketch001 = startSketchAt([-0, -0])
|> line([0, 0], %)
|> line([-4.84, -5.29], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
// this is a cryptic error message, fact that all the lines are co-linear from the `line([0,0])` is the issue why
// the close doesn't work
// when https://github.com/KittyCAD/modeling-app/issues/3268 is closed
// this test will need updating
const crypticErrorText = `ApiError`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
})
test('executes on load', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
// expand variables section
const variablesTabButton = page.getByTestId('variables-pane-button')
await variablesTabButton.click()
// can find sketch001 in the variables summary (pretty-json-container, makes sure we're not looking in the code editor)
// sketch001 only shows up in the variables summary if it's been executed
await page.waitForFunction(() => {
const variablesElement = document.querySelector(
'.pretty-json-container'
) as HTMLDivElement
return variablesElement.innerHTML.includes('sketch001')
})
await expect(
page.locator('.pretty-json-container >> text=sketch001')
).toBeVisible()
})
test('re-executes', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem('persistCode', `const myVar = 5`)
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
const variablesTabButton = page.getByTestId('variables-pane-button')
await variablesTabButton.click()
// expect to see "myVar:5"
await expect(
page.locator('.pretty-json-container >> text=myVar:5')
).toBeVisible()
// change 5 to 67
await page.getByText('const myVar').click()
await page.keyboard.press('End')
await page.keyboard.press('Backspace')
await page.keyboard.type('67')
await expect(
page.locator('.pretty-json-container >> text=myVar:67')
).toBeVisible()
})
test('ProgramMemory can be serialised', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0, 1], %)
|> line([1, 0], %)
|> line([0, -1], %)
|> close(%)
|> extrude(1, %)
|> patternLinear3d({
axis: [1, 0, 1],
repetitions: 3,
distance: 6
}, %)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
const messages: string[] = []
// Listen for all console events and push the message text to an array
page.on('console', (message) => messages.push(message.text()))
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
const forbiddenMessages = ['cannot serialize tagged newtype variant']
forbiddenMessages.forEach((forbiddenMessage) => {
messages.forEach((message) => {
expect(message).not.toContain(forbiddenMessage)
})
})
})
test('ensure the Zoo logo is not a link in browser app', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
const zooLogo = page.locator('[data-testid="app-logo"]')
// Make sure it's not a link
await expect(zooLogo).not.toHaveAttribute('href')
})
test('Position _ Is Out Of Range... regression test', async ({ page }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const exampleSketch = startSketchOn("XZ")
|> startProfileAt([0, 0], %)
|> angledLine({ angle: 50, length: 45 }, %)
|> yLineTo(0, %)
|> close(%)
|>
const example = extrude(5, exampleSketch)
shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)`
)
})
await expect(async () => {
await page.goto('/')
await u.waitForPageLoad()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible({
timeout: 1_000,
})
await page.waitForTimeout(200)
// expect it still to be there (sometimes it just clears for a bit?)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible({
timeout: 1_000,
})
}).toPass({ timeout: 40_000, intervals: [1_000] })
// error text on hover
await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token').first()).toBeVisible()
// Okay execution finished, let's start editing text below the error.
await u.codeLocator.click()
// Go to the end of the editor
// This bug happens when there is a diagnostic in the editor and you try to
// edit text below it.
// Or delete a huge chunk of text and then try to edit below it.
await page.keyboard.press('End')
await page.keyboard.down('Shift')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('End')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
await page.keyboard.type('thing: "blah"', { delay: 100 })
await page.keyboard.press('Enter')
await page.keyboard.press('ArrowLeft')
await expect(page.locator('.cm-content'))
.toContainText(`const exampleSketch = startSketchOn("XZ")
|> startProfileAt([0, 0], %)
|> angledLine({ angle: 50, length: 45 }, %)
|> yLineTo(0, %)
|> close(%)
thing: "blah"`)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
})
})

View File

@ -0,0 +1,959 @@
import { test, expect, Page } from '@playwright/test'
import {
getMovementUtils,
getUtils,
PERSIST_MODELING_CONTEXT,
setup,
tearDown,
} from './test-utils'
import { uuidv4, roundOff } from 'lib/utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Sketch tests', () => {
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
page,
context,
}) => {
const u = await getUtils(page)
const selectionsSnippets = {
startProfileAt1:
'|> startProfileAt([-width / 4 + screwRadius, height / 2], %)',
startProfileAt2: '|> startProfileAt([-width / 2, 0], %)',
startProfileAt3: '|> startProfileAt([0, thickness], %)',
}
await context.addInitScript(
async ({ startProfileAt1, startProfileAt2, startProfileAt3 }: any) => {
localStorage.setItem(
'persistCode',
`
const width = 20
const height = 10
const thickness = 5
const screwRadius = 3
const wireRadius = 2
const wireOffset = 0.5
const screwHole = startSketchOn('XY')
${startProfileAt1}
|> arc({
radius: screwRadius,
angle_start: 0,
angle_end: 360
}, %)
const part001 = startSketchOn('XY')
${startProfileAt2}
|> xLine(width * .5, %)
|> yLine(height, %)
|> xLine(-width * .5, %)
|> close(%)
|> hole(screwHole, %)
|> extrude(thickness, %)
const part002 = startSketchOn('-XZ')
${startProfileAt3}
|> xLine(width / 4, %)
|> tangentialArcTo([width / 2, 0], %)
|> xLine(-width / 4 + wireRadius, %)
|> yLine(wireOffset, %)
|> arc({
radius: wireRadius,
angle_start: 0,
angle_end: 180
}, %)
|> yLine(-wireOffset, %)
|> xLine(-width / 4, %)
|> close(%)
|> extrude(-height, %)
`
)
},
selectionsSnippets
)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await page.getByText(selectionsSnippets.startProfileAt1).click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByText(selectionsSnippets.startProfileAt2).click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByText(selectionsSnippets.startProfileAt3).click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
})
test('Can delete most of a sketch and the line tool will still work', async ({
page,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(async () => {
await page.mouse.click(700, 200)
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeEnabled({ timeout: 1000 })
await page.getByRole('button', { name: 'Edit Sketch' }).click()
}).toPass({ timeout: 40_000, intervals: [1_000] })
await page.waitForTimeout(600) // wait for animation
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
await page.keyboard.press('End')
await page.keyboard.down('Shift')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('Home')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
await u.openAndClearDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Line', exact: true }).click()
await page.waitForTimeout(100)
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([0.31, 16.47], %)`
)
})
test('Can exit selection of face', async ({ page }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem('persistCode', ``)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(page.getByText('select a plane or face')).toBeVisible()
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
})
test.describe('Can edit segments by dragging their handles', () => {
const doEditSegmentsByDraggingHandle = async (
page: Page,
openPanes: string[]
) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
await u.closeDebugPanel()
// If we have the code pane open, we should see the code.
if (openPanes.includes('code')) {
await expect(u.codeLocator)
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)`)
} else {
// Ensure we don't see the code.
await expect(u.codeLocator).not.toBeVisible()
}
const startPX = [665, 458]
const dragPX = 30
let prevContent = ''
if (openPanes.includes('code')) {
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
} else {
// Wait for the render.
await page.waitForTimeout(1000)
// Select the sketch
await page.mouse.click(700, 370)
}
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
if (openPanes.includes('code')) {
prevContent = await page.locator('.cm-content').innerText()
}
const step5 = { steps: 5 }
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
// drag startProfieAt handle
await page.mouse.move(startPX[0], startPX[1])
await page.mouse.down()
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
await page.mouse.up()
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
}
// drag line handle
await page.waitForTimeout(100)
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.mouse.move(lineEnd.x - 5, lineEnd.y)
await page.mouse.down()
await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
}
// drag tangentialArcTo handle
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
await page.mouse.move(tangentEnd.x, tangentEnd.y - 5)
await page.mouse.down()
await page.mouse.move(tangentEnd.x + dragPX, tangentEnd.y - dragPX, step5)
await page.mouse.up()
await page.waitForTimeout(100)
if (openPanes.includes('code')) {
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
}
// Open the code pane
await u.openKclCodePanel()
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([6.44, -12.07], %)
|> line([14.72, 1.97], %)
|> tangentialArcTo([24.95, -5.38], %)
|> line([1.97, 2.06], %)
|> close(%)`)
}
test('code pane open at start-handles', async ({ page }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
'store',
JSON.stringify({
state: {
openPanes: ['code'],
},
version: 0,
})
)
})
await doEditSegmentsByDraggingHandle(page, ['code'])
})
test('code pane closed at start-handles', async ({ page }) => {
// Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem(
persistModelingContext,
JSON.stringify({ openPanes: [] })
)
}, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(page, [])
})
})
// failing for the same reason as "Can undo a sketch modification with ctrl+z"
// please fix together
test.fixme(
'Can edit a sketch that has been extruded in the same pipe',
async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)
|> extrude(5, %)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
const startPX = [665, 458]
const dragPX = 40
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText()
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
// drag startProfieAt handle
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0], y: startPX[1] },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag line handle
await page.waitForTimeout(100)
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.waitForTimeout(100)
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX },
})
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag tangentialArcTo handle
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: tangentEnd.x, y: tangentEnd.y - 5 },
targetPosition: {
x: tangentEnd.x + dragPX,
y: tangentEnd.y + dragPX,
},
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -16.82], %)
|> line([15.4, -2.74], %)
|> tangentialArcTo([24.95, -5.38], %)
|> line([2.65, -2.69], %)
|> close(%)
|> extrude(5, %)`)
}
)
test('Can edit a sketch that has been revolved in the same pipe', async ({
page,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)
|> revolve({ axis: "X",}, %)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
const startPX = [665, 458]
const dragPX = 30
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText()
const step5 = { steps: 5 }
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
// drag startProfieAt handle
await page.mouse.move(startPX[0], startPX[1])
await page.mouse.down()
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
await page.mouse.up()
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag line handle
await page.waitForTimeout(100)
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.mouse.move(lineEnd.x - 5, lineEnd.y)
await page.mouse.down()
await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5)
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag tangentialArcTo handle
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
await page.mouse.move(tangentEnd.x, tangentEnd.y - 5)
await page.mouse.down()
await page.mouse.move(tangentEnd.x + dragPX, tangentEnd.y - dragPX, step5)
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([6.44, -12.07], %)
|> line([14.72, 1.97], %)
|> tangentialArcTo([24.95, -5.38], %)
|> line([1.97, 2.06], %)
|> close(%)
|> revolve({ axis: "X" }, %)`)
})
test('Can add multiple sketches', async ({ page }) => {
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize)
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
const { toSU, click00r } = getMovementUtils({ center, page })
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
let codeStr = "const sketch001 = startSketchOn('XY')"
await page.mouse.click(center.x, viewportSize.height * 0.55)
await expect(u.codeLocator).toHaveText(codeStr)
await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await click00r(0, 0)
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(50, 0)
await page.waitForTimeout(100)
codeStr += ` |> line(${toSU([50, 0])}, %)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(0, 50)
codeStr += ` |> line(${toSU([0, 50])}, %)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(-50, 0)
codeStr += ` |> line(${toSU([-50, 0])}, %)`
await expect(u.codeLocator).toHaveText(codeStr)
// exit the sketch, reset relative clicker
click00r(undefined, undefined)
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await page.waitForTimeout(250)
await u.clearCommandLogs()
// start a new sketch
await page.getByRole('button', { name: 'Start Sketch' }).click()
// when exiting the sketch above the camera is still looking down at XY,
// so selecting the plane again is a bit easier.
await page.mouse.click(center.x + 200, center.y + 100)
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
codeStr += "const sketch002 = startSketchOn('XY')"
await expect(u.codeLocator).toHaveText(codeStr)
await u.closeDebugPanel()
await click00r(30, 0)
codeStr += ` |> startProfileAt([1.53, 0], %)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(30, 0)
codeStr += ` |> line([1.53, 0], %)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(0, 30)
codeStr += ` |> line([0, -1.53], %)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(-30, 0)
codeStr += ` |> line([-1.53, 0], %)`
await expect(u.codeLocator).toHaveText(codeStr)
await click00r(undefined, undefined)
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.updateCamPosition([100, 100, 100])
await u.clearCommandLogs()
})
test.describe('Snap to close works (at any scale)', () => {
const doSnapAtDifferentScales = async (
page: any,
camPos: [number, number, number],
scale = 1
) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
const code = `const sketch001 = startSketchOn('-XZ')
|> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %)
|> line([${roundOff(scale * 139.19)}, 0], %)
|> line([0, -${roundOff(scale * 139.2)}], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.updateCamPosition(camPos)
await u.closeDebugPanel()
await page.mouse.move(0, 0)
// select a plane
await page.mouse.move(700, 200, { steps: 10 })
await page.mouse.click(700, 200, { delay: 200 })
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('-XZ')`
)
let prevContent = await page.locator('.cm-content').innerText()
const pointA = [700, 200]
const pointB = [900, 200]
const pointC = [900, 400]
// draw three lines
await page.waitForTimeout(500)
await page.mouse.move(pointA[0], pointA[1], { steps: 10 })
await page.mouse.click(pointA[0], pointA[1], { delay: 200 })
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.move(pointB[0], pointB[1], { steps: 10 })
await page.mouse.click(pointB[0], pointB[1], { delay: 200 })
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.move(pointC[0], pointC[1], { steps: 10 })
await page.mouse.click(pointC[0], pointC[1], { delay: 200 })
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await page.mouse.move(pointA[0] - 12, pointA[1] + 12, { steps: 10 })
const pointNotQuiteA = [pointA[0] - 7, pointA[1] + 7]
await page.mouse.move(pointNotQuiteA[0], pointNotQuiteA[1], { steps: 10 })
await page.mouse.click(pointNotQuiteA[0], pointNotQuiteA[1], {
delay: 200,
})
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
await expect(page.locator('.cm-content')).toHaveText(code)
// Assert the tool was unequipped
await expect(
page.getByRole('button', { name: 'Line', exact: true })
).not.toHaveAttribute('aria-pressed', 'true')
// exit sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.removeCurrentCode()
}
test('[0, 100, 100]', async ({ page }) => {
await doSnapAtDifferentScales(page, [0, 100, 100], 0.01)
})
test('[0, 10000, 10000]', async ({ page }) => {
await doSnapAtDifferentScales(page, [0, 10000, 10000])
})
})
test('exiting a close extrude, has the extrude button enabled ready to go', async ({
page,
}) => {
// this was a regression https://github.com/KittyCAD/modeling-app/issues/2832
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([-0.45, 0.87], %)
|> line([1.32, 0.38], %)
|> line([1.02, -1.32], %, $seg01)
|> line([-1.01, -0.77], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// click "line([1.32, 0.38], %)"
await page.getByText(`line([1.32, 0.38], %)`).click()
await page.waitForTimeout(100)
// click edit sketch
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(600) // wait for animation
// exit sketch
await page.getByRole('button', { name: 'Exit Sketch' }).click()
// expect extrude button to be enabled
await expect(
page.getByRole('button', { name: 'Extrude' })
).not.toBeDisabled()
// click extrude
await page.getByRole('button', { name: 'Extrude' }).click()
// sketch selection should already have been made. "Selection: 1 face" only show up when the selection has been made already
// otherwise the cmdbar would be waiting for a selection.
await expect(
page.getByRole('button', { name: 'selection : 1 face', exact: false })
).toBeVisible()
})
test("Existing sketch with bad code delete user's code", async ({ page }) => {
// this was a regression https://github.com/KittyCAD/modeling-app/issues/2832
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([-0.45, 0.87], %)
|> line([1.32, 0.38], %)
|> line([1.02, -1.32], %, $seg01)
|> line([-1.01, -0.77], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(5, sketch001)
`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.mouse.click(622, 355)
await page.waitForTimeout(800)
await page.getByText(`END')`).click()
await page.keyboard.press('End')
await page.keyboard.press('Enter')
await page.keyboard.type(' |>', { delay: 100 })
await page.waitForTimeout(100)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
await expect((await u.codeLocator.innerText()).replace(/\s/g, '')).toBe(
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([-0.45, 0.87], %)
|> line([1.32, 0.38], %)
|> line([1.02, -1.32], %, $seg01)
|> line([-1.01, -0.77], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(5, sketch001)
const sketch002 = startSketchOn(extrude001, 'END')
|>
`.replace(/\s/g, '')
)
})
test('empty-scene default-planes act as expected', async ({
page,
browserName,
}) => {
test.skip(
browserName === 'webkit',
'Skip on Safari until `window.tearDown` is working there'
)
/**
* Tests the following things
* 1) The the planes are there on load because the scene is empty
* 2) The planes don't changes color when hovered initially
* 3) Putting something in the scene makes the planes hidden
* 4) Removing everything from the scene shows the plans again
* 3) Once "start sketch" is click, the planes do respond to hovers
* 4) Selecting a plan works as expected, i.e. sketch mode
* 5) Reloading the scene with something already in the scene means the planes are hidden
*/
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const XYPlanePoint = { x: 774, y: 116 } as const
const unHoveredColor: [number, number, number] = [47, 47, 93]
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await u.openAndClearDebugPanel()
await u.codeLocator.fill(`const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> xLine(-20, %)
`)
await u.expectCmdLog('[data-message-type="execution-done"]')
const noPlanesColor: [number, number, number] = [30, 30, 30]
expect(
await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor)
).toBeLessThan(3)
await u.clearCommandLogs()
await u.removeCurrentCode()
await u.expectCmdLog('[data-message-type="execution-done"]')
await expect
.poll(() => u.getGreatestPixDiff(XYPlanePoint, unHoveredColor), {
timeout: 5_000,
})
.toBeLessThan(8)
// click start Sketch
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y, { steps: 5 })
const hoveredColor: [number, number, number] = [93, 93, 127]
// now that we're expecting the user to select a plan, it does respond to hover
await expect
.poll(() => u.getGreatestPixDiff(XYPlanePoint, hoveredColor))
.toBeLessThan(8)
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(600)
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50)
await expect(u.codeLocator)
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([11.8, 9.09], %)
|> line([3.39, -3.39], %)
`)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([11.8, 9.09], %)
|> line([3.39, -3.39], %)
`
)
})
await page.reload()
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// expect there to be no planes on load since there's something in the scene
expect(
await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor)
).toBeLessThan(3)
})
})

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -86,3 +86,285 @@ export const TEST_CODE_GIZMO = `const part001 = startSketchOn('XZ')
|> close(%)
|> extrude(5 + 7, %)
`
export const TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW = `const width = 50.8
const height = 30
const thickness = 2
const keychainHoleSize = 3
const keychain = startSketchOn("XY")
|> startProfileAt([0, 0], %)
|> lineTo([width, 0], %)
|> lineTo([width, height], %)
|> lineTo([0, height], %)
|> close(%)
|> extrude(thickness, %)
// generated from /home/paultag/Downloads/zma-logomark.svg
fn svg = (surface, origin, depth) => {
let a0 = surface |> startProfileAt([origin[0] + 45.430427, origin[1] + -14.627736], %)
|> bezierCurve({
control1: [ 0, 0.764157 ],
control2: [ 0, 1.528314 ],
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, %)
let a1 = surface |> startProfileAt([origin[0] + 57.920488, origin[1] + -15.244943], %)
|> bezierCurve({
control1: [ -2.78904, 0.106635 ],
control2: [ -5.052548, -2.969529 ],
to: [ -4.055141, -5.598369 ]
}, %)
|> 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, %)
const box = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %, $revolveAxis)
|> close(%)
|> extrude(10, %)
const sketch001 = startSketchOn(box, revolveAxis)
|> startProfileAt([5, 10], %)
|> line([0, -10], %)
|> line([2, 0], %)
|> line([0, -10], %)
|> close(%)
|> revolve({
axis: revolveAxis,
angle: 90
}, %)
return 0
}
svg(startSketchOn(keychain, 'end'), [-33, 32], -thickness)
startSketchOn(keychain, 'end')
|> circle([
width / 2,
height - (keychainHoleSize + 1.5)
], keychainHoleSize, %)
|> extrude(-thickness, %)`

View File

@ -0,0 +1,237 @@
import { test, expect } from '@playwright/test'
import { commonPoints, getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Test network and connection issues', () => {
test('simulate network down and network little widget', async ({
page,
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)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
const networkToggle = page.getByTestId('network-toggle')
// This is how we wait until the stream is online
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
const networkWidget = page.locator('[data-testid="network-toggle"]')
await expect(networkWidget).toBeVisible()
await networkWidget.hover()
const networkPopover = page.locator('[data-testid="network-popover"]')
await expect(networkPopover).not.toBeVisible()
// (First check) Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// Click the network widget
await networkWidget.click()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Click off the modal.
await page.mouse.click(100, 100)
await expect(networkPopover).not.toBeVisible()
// Turn off the network
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Expect the network to be down
await expect(networkToggle).toContainText('Offline')
// Click the network widget
await networkWidget.click()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Click off the modal.
await page.mouse.click(0, 0)
await expect(networkPopover).not.toBeVisible()
// Turn back on the network
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// (Second check) expect the network to be up
await expect(networkToggle).toContainText('Connected')
})
test('Engine disconnect & reconnect in sketch mode', async ({
page,
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 networkToggle = page.getByTestId('network-toggle')
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// click on "Start Sketch" button
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')`
)
await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// simulate network down
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Expect the network to be down
await expect(networkToggle).toContainText('Offline')
// Ensure we are not in sketch mode
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// simulate network up
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Wait for the app to be ready for use
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
await expect(page.getByTestId('loading-stream')).not.toBeAttached()
// Click off the code pane.
await page.mouse.click(100, 100)
// select a line
await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click()
// enter sketch again
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings'
)
await page.waitForTimeout(150)
// Click the line tool
await page.getByRole('button', { name: 'Line', exact: true }).click()
await page.waitForTimeout(150)
// Ensure we can continue sketching
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([-8.84, 8.75], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([-8.84, 8.75], %)
|> line([-5.6, 0], %)`)
// Unequip line tool
await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode.
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'Line', exact: true })
).not.toHaveAttribute('aria-pressed', 'true')
// Exit sketch
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
})
})

View File

@ -1,5 +1,11 @@
import { expect, Page, Download } from '@playwright/test'
import { EngineCommand } from 'lang/std/artifactMap'
import {
expect,
Page,
Download,
TestInfo,
BrowserContext,
} from '@playwright/test'
import { EngineCommand } from 'lang/std/artifactGraph'
import os from 'os'
import fsp from 'fs/promises'
import pixelMatch from 'pixelmatch'
@ -7,6 +13,10 @@ import { PNG } from 'pngjs'
import { Protocol } from 'playwright-core/types/protocol'
import type { Models } from '@kittycad/lib'
import { APP_NAME } from 'lib/constants'
import waitOn from 'wait-on'
import { secrets } from './secrets'
import { TEST_SETTINGS_KEY, TEST_SETTINGS } from './storageStates'
import * as TOML from '@iarna/toml'
type TestColor = [number, number, number]
export const TEST_COLORS = {
@ -15,6 +25,33 @@ export const TEST_COLORS = {
BLUE: [0, 0, 255] as TestColor,
} as const
export const PERSIST_MODELING_CONTEXT = 'persistModelingContext'
export const deg = (Math.PI * 2) / 360
export const commonPoints = {
startAt: '[7.19, -9.7]',
num1: 7.25,
num2: 14.44,
}
async function waitForPageLoadWithRetry(page: Page) {
await expect(async () => {
await page.goto('/')
const errorMessage = 'App failed to load - 🔃 Retrying ...'
await expect(page.getByTestId('loading'), errorMessage).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' }),
errorMessage
).toBeEnabled({
timeout: 20_000,
})
}).toPass({ timeout: 70_000, intervals: [1_000] })
}
async function waitForPageLoad(page: Page) {
// wait for all spinners to be gone
await expect(page.getByTestId('loading')).not.toBeAttached({
@ -218,9 +255,12 @@ async function waitForAuthAndLsp(page: Page) {
}
return false
})
await page.goto('/')
await waitForPageLoad(page)
if (process.env.CI) {
await waitForPageLoadWithRetry(page)
} else {
await page.goto('/')
await waitForPageLoad(page)
}
return waitForLspPromise
}
@ -234,6 +274,7 @@ export async function getUtils(page: Page) {
return {
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
waitForPageLoad: () => waitForPageLoad(page),
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
removeCurrentCode: () => removeCurrentCode(page),
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
updateCamPosition: async (xyz: [number, number, number]) => {
@ -542,3 +583,46 @@ export const doExport = async (
* Gets the appropriate modifier key for the platform.
*/
export const metaModifier = os.platform() === 'darwin' ? 'Meta' : 'Control'
export async function tearDown(page: Page, testInfo: TestInfo) {
if (testInfo.status === 'skipped') return
if (testInfo.status === 'failed') return
const u = await getUtils(page)
// Kill the network so shutdown happens properly
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// It seems it's best to give the browser about 3s to close things
// It's not super reliable but we have no real other choice for now
await page.waitForTimeout(3000)
}
export async function setup(context: BrowserContext, page: Page) {
// wait for Vite preview server to be up
await waitOn({
resources: ['tcp:3000'],
timeout: 5000,
})
await context.addInitScript(
async ({ token, settingsKey, settings }) => {
localStorage.setItem('TOKEN_PERSIST_KEY', token)
localStorage.setItem('persistCode', ``)
localStorage.setItem(settingsKey, settings)
localStorage.setItem('playwright', 'true')
},
{
token: secrets.token,
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: TEST_SETTINGS }),
}
)
// kill animations, speeds up tests and reduced flakiness
await page.emulateMedia({ reducedMotion: 'reduce' })
}

View File

@ -0,0 +1,339 @@
import { test, expect } from '@playwright/test'
import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils'
import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Testing Camera Movement', () => {
test('Can moving camera', async ({ page, context }) => {
test.skip(process.platform === 'darwin', 'Can moving camera')
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel()
await u.closeKclCodePanel()
const camPos: [number, number, number] = [0, 85, 85]
const bakeInRetries = async (
mouseActions: any,
xyz: [number, number, number],
cnt = 0
) => {
// hack that we're implemented our own retry instead of using retries built into playwright.
// however each of these camera drags can be flaky, because of udp
// and so putting them together means only one needs to fail to make this test extra flaky.
// this way we can retry within the test
// We could break them out into separate tests, but the longest past of the test is waiting
// for the stream to start, so it can be good to bundle related things together.
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: camPos[0], y: camPos[1], z: camPos[2] },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
// rotate
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// const yo = page.getByTestId('cam-x-position').inputValue()
await u.doAndWaitForImageDiff(async () => {
await mouseActions()
await u.openAndClearDebugPanel()
await u.closeDebugPanel()
await page.waitForTimeout(100)
}, 300)
await u.openAndClearDebugPanel()
await page.getByTestId('cam-x-position').isVisible()
const vals = await Promise.all([
page.getByTestId('cam-x-position').inputValue(),
page.getByTestId('cam-y-position').inputValue(),
page.getByTestId('cam-z-position').inputValue(),
])
const xError = Math.abs(Number(vals[0]) + xyz[0])
const yError = Math.abs(Number(vals[1]) + xyz[1])
const zError = Math.abs(Number(vals[2]) + xyz[2])
let shouldRetry = false
if (xError > 5 || yError > 5 || zError > 5) {
if (cnt > 2) {
console.log('xVal', vals[0], 'xError', xError)
console.log('yVal', vals[1], 'yError', yError)
console.log('zVal', vals[2], 'zError', zError)
throw new Error('Camera position not as expected')
}
shouldRetry = true
}
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(100)
if (shouldRetry) await bakeInRetries(mouseActions, xyz, cnt + 1)
}
await bakeInRetries(async () => {
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(600, 303)
await page.mouse.up({ button: 'right' })
}, [4, -10.5, -120])
await bakeInRetries(async () => {
await page.keyboard.down('Shift')
await page.mouse.move(600, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 200, { steps: 2 })
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
}, [-19, -85, -85])
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: camPos[0], y: camPos[1], z: camPos[2] },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
await u.clearCommandLogs()
await u.closeDebugPanel()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(200)
// zoom
await u.doAndWaitForImageDiff(async () => {
await page.keyboard.down('Control')
await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 300)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
await u.openDebugPanel()
await page.waitForTimeout(300)
await u.clearCommandLogs()
await u.closeDebugPanel()
}, 300)
// zoom with scroll
await u.openAndClearDebugPanel()
// TODO, it appears we don't get the cam setting back from the engine when the interaction is zoom into `backInRetries` once the information is sent back on zoom
// await expect(Math.abs(Number(await page.getByTestId('cam-x-position').inputValue()) + 12)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-y-position').inputValue()) - 85)).toBeLessThan(1.5)
// await expect(Math.abs(Number(await page.getByTestId('cam-z-position').inputValue()) - 85)).toBeLessThan(1.5)
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await bakeInRetries(async () => {
await page.mouse.move(700, 400)
await page.mouse.wheel(0, -100)
}, [0, -85, -85])
})
test('Zoom should be consistent when exiting or entering sketches', async ({
page,
}) => {
// start new sketch pan and zoom before exiting, when exiting the sketch should stay in the same place
// than zoom and pan outside of sketch mode and enter again and it should not change from where it is
// than again for sketching
test.skip(process.platform !== 'darwin', 'Zoom should be consistent')
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// select a plane
await page.mouse.click(700, 325)
let code = `const sketch001 = startSketchOn('XY')`
await expect(u.codeLocator).toHaveText(code)
await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
// move the camera slightly
await page.keyboard.down('Shift')
await page.mouse.move(700, 300)
await page.mouse.down({ button: 'right' })
await page.mouse.move(800, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
let y = 350,
x = 948
await u.canvasLocator.click({ position: { x: 783, y } })
code += `\n |> startProfileAt([8.12, -12.98], %)`
// await expect(u.codeLocator).toHaveText(code)
await u.canvasLocator.click({ position: { x, y } })
code += `\n |> line([11.18, 0], %)`
// await expect(u.codeLocator).toHaveText(code)
await u.canvasLocator.click({ position: { x, y: 275 } })
code += `\n |> line([0, 6.99], %)`
// await expect(u.codeLocator).toHaveText(code)
// click the line button
await page.getByRole('button', { name: 'Line', exact: true }).click()
const hoverOverNothing = async () => {
// await u.canvasLocator.hover({position: {x: 700, y: 325}})
await page.mouse.move(700, 325)
await page.waitForTimeout(100)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible({
timeout: 10_000,
})
}
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.waitForTimeout(200)
// hover over horizontal line
await u.canvasLocator.hover({ position: { x: 800, y } })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await page.waitForTimeout(200)
await hoverOverNothing()
await page.waitForTimeout(200)
// hover over vertical line
await u.canvasLocator.hover({ position: { x, y: 325 } })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// click exit sketch
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(400)
await hoverOverNothing()
await page.waitForTimeout(200)
// hover over horizontal line
await page.mouse.move(858, y, { steps: 5 })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// hover over vertical line
await page.mouse.move(x, 325)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
// hover over vertical line
await page.mouse.move(857, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
// now click it
await page.mouse.click(857, y)
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
await hoverOverNothing()
x = 975
y = 468
await page.waitForTimeout(100)
await page.mouse.move(x, 419, { steps: 5 })
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.mouse.move(855, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await page.waitForTimeout(200)
await hoverOverNothing()
await page.waitForTimeout(200)
await page.mouse.move(x, 419)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
await hoverOverNothing()
await page.mouse.move(855, y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
timeout: 10_000,
})
})
})

View File

@ -0,0 +1,938 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown, TEST_COLORS } from './test-utils'
import { XOR } from 'lib/utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Testing constraints', () => {
test('Can constrain line length', async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> xLine(-20, %)
`
)
})
const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// Click the line of code for line.
await page.getByText(`line([0, 20], %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.waitForTimeout(100)
// enter sketch again
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500) // wait for animation
const startXPx = 500
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.keyboard.down('Shift')
await page.mouse.click(834, 244)
await page.keyboard.up('Shift')
await page.getByRole('button', { name: 'Length', exact: true }).click()
await page.getByText('Add constraining value').click()
await expect(page.locator('.cm-content')).toHaveText(
`const length001 = 20const sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
)
// Make sure we didn't pop out of sketch mode.
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await page.waitForTimeout(500) // wait for animation
// Exit sketch
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
})
test(`Test remove constraints`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const yo = 79
const part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %, $seg01)
|> line([78.92, -120.11], %)
|> angledLine([segAng(seg01), yo], %)
|> line([41.19, 28.97 + 5], %)
const part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %, $seg01)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const line3 = await u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`)
await page.mouse.click(line3.x, line3.y)
await page.waitForTimeout(100) // this wait is needed for webkit - not sure why
await page
.getByRole('button', {
name: 'Length: open menu',
})
.click()
await page.getByRole('button', { name: 'remove constraints' }).click()
await page.getByText('line([39.13, 68.63], %)').click()
const activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent).toHaveLength(1)
await expect(activeLinesContent[0]).toHaveText('|> line([39.13, 68.63], %)')
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
})
test.describe('Test perpendicular distance constraint', () => {
const cases = [
{
testName: 'Add variable',
offset: '-offset001',
},
{
testName: 'No variable',
offset: '-128.05',
},
] as const
for (const { testName, offset } of cases) {
test(`${testName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const yo = 5
const part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %, $seg01)
|> line([78.92, -120.11], %)
|> angledLine([segAng(seg01), 78.33], %)
|> line([41.19, 28.97], %)
const part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %, $seg01)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const [line1, line3] = await Promise.all([
u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`),
u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`),
])
await page.mouse.click(line1.x, line1.y)
await page.keyboard.down('Shift')
await page.mouse.click(line3.x, line3.y)
await page.waitForTimeout(100) // this wait is needed for webkit - not sure why
await page.keyboard.up('Shift')
await page
.getByRole('button', {
name: 'Length: open menu',
})
.click()
await page
.getByRole('button', { name: 'Perpendicular Distance' })
.click()
const createNewVariableCheckbox = page.getByTestId(
'create-new-variable-checkbox'
)
const isChecked = await createNewVariableCheckbox.isChecked()
const addVariable = testName === 'Add variable'
XOR(isChecked, addVariable) && // XOR because no need to click the checkbox if the state is already correct
(await createNewVariableCheckbox.click())
await page
.getByRole('button', { name: 'Add constraining value' })
.click()
// Wait for the codemod to take effect
await expect(page.locator('.cm-content')).toContainText(`angle: -57,`)
await expect(page.locator('.cm-content')).toContainText(
`offset: ${offset},`
)
const activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(
`|> line([74.36, 130.4], %, $seg01)`
)
await expect(activeLinesContent[1]).toHaveText(`}, %)`)
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
})
}
})
test.describe('Test distance between constraint', () => {
const cases = [
{
testName: 'Add variable',
constraint: 'horizontal distance',
value: 'segEndX(seg01) + xDis001, 61.34',
},
{
testName: 'No variable',
constraint: 'horizontal distance',
value: 'segEndX(seg01) + 88.08, 61.34',
},
{
testName: 'Add variable',
constraint: 'vertical distance',
value: '154.9, segEndY(seg01) - yDis001',
},
{
testName: 'No variable',
constraint: 'vertical distance',
value: '154.9, segEndY(seg01) - 42.32',
},
] as const
for (const { testName, value, constraint } of cases) {
test(`${constraint} - ${testName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const yo = 5
const part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([41.19, 28.97], %)
const part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const [line1, line3] = await Promise.all([
u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`),
u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`),
])
await page.mouse.click(line1.x, line1.y)
await page.keyboard.down('Shift')
await page.mouse.click(line3.x, line3.y)
await page.waitForTimeout(100) // this wait is needed for webkit - not sure why
await page.keyboard.up('Shift')
await page
.getByRole('button', {
name: 'Length: open menu',
})
.click()
await page.getByRole('button', { name: constraint }).click()
const createNewVariableCheckbox = page.getByTestId(
'create-new-variable-checkbox'
)
const isChecked = await createNewVariableCheckbox.isChecked()
const addVariable = testName === 'Add variable'
XOR(isChecked, addVariable) && // XOR because no need to click the checkbox if the state is already correct
(await createNewVariableCheckbox.click())
await page
.getByRole('button', { name: 'Add constraining value' })
.click()
// checking activeLines assures the cursors are where they should be
const codeAfter = [
`|> line([74.36, 130.4], %, $seg01)`,
`|> lineTo([${value}], %)`,
]
const activeLinesContent = await page.locator('.cm-activeLine').all()
await Promise.all(
activeLinesContent.map(async (line, i) => {
await expect(page.locator('.cm-content')).toContainText(
codeAfter[i]
)
// if the code is an active line then the cursor should be on that line
await expect(line).toHaveText(codeAfter[i])
})
)
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
})
}
})
test.describe('Test ABS distance constraint', () => {
const cases = [
{
testName: 'Add variable',
addVariable: true,
constraint: 'Absolute X',
value: 'xDis001, 61.34',
},
{
testName: 'No variable',
addVariable: false,
constraint: 'Absolute X',
value: '154.9, 61.34',
},
{
testName: 'Add variable',
addVariable: true,
constraint: 'Absolute Y',
value: '154.9, yDis001',
},
{
testName: 'No variable',
addVariable: false,
constraint: 'Absolute Y',
value: '154.9, 61.34',
},
] as const
for (const { testName, addVariable, value, constraint } of cases) {
test(`${constraint} - ${testName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const yo = 5
const part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([41.19, 28.97], %)
const part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const [line3] = await Promise.all([
u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`),
])
if (constraint === 'Absolute X') {
await page.mouse.click(600, 130)
} else {
await page.mouse.click(900, 250)
}
await page.keyboard.down('Shift')
await page.mouse.click(line3.x, line3.y)
await page.waitForTimeout(100) // this wait is needed for webkit - not sure why
await page.keyboard.up('Shift')
await page
.getByRole('button', {
name: 'Length: open menu',
})
.click()
await page
.getByRole('button', { name: constraint, exact: true })
.click()
const createNewVariableCheckbox = page.getByTestId(
'create-new-variable-checkbox'
)
const isChecked = await createNewVariableCheckbox.isChecked()
XOR(isChecked, addVariable) && // XOR because no need to click the checkbox if the state is already correct
(await createNewVariableCheckbox.click())
await page
.getByRole('button', { name: 'Add constraining value' })
.click()
// checking activeLines assures the cursors are where they should be
const codeAfter = [`|> lineTo([${value}], %)`]
const activeLinesContent = await page.locator('.cm-activeLine').all()
await Promise.all(
activeLinesContent.map(async (line, i) => {
await expect(page.locator('.cm-content')).toContainText(
codeAfter[i]
)
// if the code is an active line then the cursor should be on that line
await expect(line).toHaveText(codeAfter[i])
})
)
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
})
}
})
test.describe('Test Angle constraint double segment selection', () => {
const cases = [
{
testName: 'Add variable',
addVariable: true,
axisSelect: false,
value: 'segAng(seg01) + angle001',
},
{
testName: 'No variable',
addVariable: false,
axisSelect: false,
value: 'segAng(seg01) + 22.69',
},
{
testName: 'Add variable, selecting axis',
addVariable: true,
axisSelect: true,
value: 'QUARTER_TURN - angle001',
},
{
testName: 'No variable, selecting axis',
addVariable: false,
axisSelect: true,
value: 'QUARTER_TURN - 7',
},
] as const
for (const { testName, addVariable, value, axisSelect } of cases) {
test(`${testName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const yo = 5
const part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([41.19, 28.97], %)
const part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const [line1, line3] = await Promise.all([
u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`),
u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`),
])
if (axisSelect) {
await page.mouse.click(600, 130)
} else {
await page.mouse.click(line1.x, line1.y)
}
await page.keyboard.down('Shift')
await page.mouse.click(line3.x, line3.y)
await page.waitForTimeout(100) // this wait is needed for webkit - not sure why
await page.keyboard.up('Shift')
await page
.getByRole('button', {
name: 'Length: open menu',
})
.click()
await page.getByTestId('dropdown-constraint-angle').click()
const createNewVariableCheckbox = page.getByTestId(
'create-new-variable-checkbox'
)
const isChecked = await createNewVariableCheckbox.isChecked()
XOR(isChecked, addVariable) && // XOR because no need to click the checkbox if the state is already correct
(await createNewVariableCheckbox.click())
await page
.getByRole('button', { name: 'Add constraining value' })
.click()
// checking activeLines assures the cursors are where they should be
const codeAfter = [
'|> line([74.36, 130.4], %, $seg01)',
`|> angledLine([${value}, 78.33], %)`,
]
if (axisSelect) codeAfter.shift()
const activeLinesContent = await page.locator('.cm-activeLine').all()
await Promise.all(
activeLinesContent.map(async (line, i) => {
await expect(page.locator('.cm-content')).toContainText(
codeAfter[i]
)
// if the code is an active line then the cursor should be on that line
await expect(line).toHaveText(codeAfter[i])
})
)
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
})
}
})
test.describe('Test Angle/Length constraint single selection', () => {
const cases = [
{
testName: 'Angle - Add variable',
addVariable: true,
constraint: 'angle',
value: 'angle001, 78.33',
},
{
testName: 'Angle - No variable',
addVariable: false,
constraint: 'angle',
value: '83, 78.33',
},
{
testName: 'Length - Add variable',
addVariable: true,
constraint: 'length',
value: '83, length001',
},
{
testName: 'Length - No variable',
addVariable: false,
constraint: 'length',
value: '83, 78.33',
},
] as const
for (const { testName, addVariable, value, constraint } of cases) {
test(`${testName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const yo = 5
const part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([41.19, 28.97], %)
const part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const line3 = await u.getSegmentBodyCoords(
`[data-overlay-index="${2}"]`
)
await page.mouse.click(line3.x, line3.y)
await page
.getByRole('button', {
name: 'Length: open menu',
})
.click()
await page.getByTestId('dropdown-constraint-' + constraint).click()
if (!addVariable) {
await page.getByTestId('create-new-variable-checkbox').click()
}
await page
.getByRole('button', { name: 'Add constraining value' })
.click()
const changedCode = `|> angledLine([${value}], %)`
await expect(page.locator('.cm-content')).toContainText(changedCode)
// checking active assures the cursor is where it should be
await expect(page.locator('.cm-activeLine')).toHaveText(changedCode)
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
})
}
})
test.describe('Many segments - no modal constraints', () => {
const cases = [
{
constraintName: 'Vertical',
codeAfter: [
`|> yLine(130.4, %)`,
`|> yLine(77.79, %)`,
`|> yLine(28.97, %)`,
],
},
{
codeAfter: [
`|> xLine(74.36, %)`,
`|> xLine(9.16, %)`,
`|> xLine(41.19, %)`,
],
constraintName: 'Horizontal',
},
] as const
for (const { codeAfter, constraintName } of cases) {
test(`${constraintName}`, async ({ page }) => {
await page.addInitScript(async (customCode) => {
localStorage.setItem(
'persistCode',
`const yo = 5
const part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([41.19, 28.97], %)
const part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const line1 = await u.getSegmentBodyCoords(
`[data-overlay-index="${0}"]`
)
const line3 = await u.getSegmentBodyCoords(
`[data-overlay-index="${2}"]`
)
const line4 = await u.getSegmentBodyCoords(
`[data-overlay-index="${3}"]`
)
// select two segments by holding down shift
await page.mouse.click(line1.x, line1.y)
await page.keyboard.down('Shift')
await page.mouse.click(line3.x, line3.y)
await page.mouse.click(line4.x, line4.y)
await page.keyboard.up('Shift')
// check actives lines
const activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent).toHaveLength(codeAfter.length)
const constraintMenuButton = page.getByRole('button', {
name: 'Length: open menu',
})
const constraintButton = page
.getByRole('button', {
name: constraintName,
})
.first()
// apply the constraint
await constraintMenuButton.click()
await constraintButton.click({ delay: 200 })
// check there are still 3 cursors (they should stay on the same lines as before constraint was applied)
await expect(page.locator('.cm-cursor')).toHaveCount(codeAfter.length)
// check both cursors are where they should be after constraint is applied and the code is correct
await Promise.all(
activeLinesContent.map(async (line, i) => {
await expect(page.locator('.cm-content')).toContainText(
codeAfter[i]
)
// if the code is an active line then the cursor should be on that line
await expect(line).toHaveText(codeAfter[i])
})
)
})
}
})
test.describe('Two segment - no modal constraints', () => {
const cases = [
{
codeAfter: `|> angledLine([83, segLen(seg01)], %)`,
constraintName: 'Equal Length',
},
{
codeAfter: `|> angledLine([segAng(seg01), 78.33], %)`,
constraintName: 'Parallel',
},
{
codeAfter: `|> lineTo([segEndX(seg01), 61.34], %)`,
constraintName: 'Vertically Align',
},
{
codeAfter: `|> lineTo([154.9, segEndY(seg01)], %)`,
constraintName: 'Horizontally Align',
},
] as const
for (const { codeAfter, constraintName } of cases) {
test(`${constraintName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const yo = 5
const part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
const part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const line1 = await u.getBoundingBox(`[data-overlay-index="${0}"]`)
const line3 = await u.getBoundingBox(`[data-overlay-index="${2}"]`)
// select two segments by holding down shift
await page.mouse.click(line1.x - 20, line1.y + 20)
await page.keyboard.down('Shift')
await page.mouse.click(line3.x - 3, line3.y + 20)
await page.keyboard.up('Shift')
const constraintMenuButton = page.getByRole('button', {
name: 'Length: open menu',
})
const constraintButton = page.getByRole('button', {
name: constraintName,
})
// apply the constraint
await constraintMenuButton.click()
await constraintButton.click()
await expect(page.locator('.cm-content')).toContainText(codeAfter)
// expect the string 'seg01' to appear twice in '.cm-content' the tag segment and referencing the tag
const content = await page.locator('.cm-content').innerText()
await expect(content.match(/seg01/g)).toHaveLength(2)
// check there are still 2 cursors (they should stay on the same lines as before constraint was applied)
await expect(page.locator('.cm-cursor')).toHaveCount(2)
// check actives lines
const activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent).toHaveLength(2)
// check both cursors are where they should be after constraint is applied
await expect(activeLinesContent[0]).toHaveText(
'|> line([74.36, 130.4], %, $seg01)'
)
await expect(activeLinesContent[1]).toHaveText(codeAfter)
})
}
})
test.describe('Axis & segment - no modal constraints', () => {
const cases = [
{
codeAfter: `|> lineTo([154.9, ZERO], %)`,
axisClick: { x: 950, y: 250 },
constraintName: 'Snap To X',
},
{
codeAfter: `|> lineTo([ZERO, 61.34], %)`,
axisClick: { x: 600, y: 150 },
constraintName: 'Snap To Y',
},
] as const
for (const { codeAfter, constraintName, axisClick } of cases) {
test(`${constraintName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const yo = 5
const part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
const part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const line3 = await u.getBoundingBox(`[data-overlay-index="${2}"]`)
// select segment and axis by holding down shift
await page.mouse.click(line3.x - 3, line3.y + 20)
await page.keyboard.down('Shift')
await page.waitForTimeout(100)
await page.mouse.click(axisClick.x, axisClick.y)
await page.keyboard.up('Shift')
const constraintMenuButton = page.getByRole('button', {
name: 'Length: open menu',
})
const constraintButton = page.getByRole('button', {
name: constraintName,
})
// apply the constraint
await constraintMenuButton.click()
await expect(constraintButton).toBeVisible()
await constraintButton.click()
// check the cursor is where is should be after constraint is applied
await expect(page.locator('.cm-content')).toContainText(codeAfter)
await expect(page.locator('.cm-activeLine')).toHaveText(codeAfter)
})
}
})
test('Horizontally constrained line remains selected after applying constraint', async ({
page,
}) => {
test.setTimeout(70_000)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XY')
|> startProfileAt([-1.05, -1.07], %)
|> line([3.79, 2.68], %, $seg01)
|> line([3.13, -2.4], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([3.79, 2.68], %, $seg01)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeEnabled(
{ timeout: 10_000 }
)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(100)
const lineBefore = await u.getSegmentBodyCoords(
`[data-overlay-index="1"]`,
0
)
expect(
await u.getGreatestPixDiff(lineBefore, TEST_COLORS.WHITE)
).toBeLessThan(3)
await page.mouse.move(lineBefore.x, lineBefore.y)
await page.waitForTimeout(50)
await page.mouse.click(lineBefore.x, lineBefore.y)
expect(
await u.getGreatestPixDiff(lineBefore, TEST_COLORS.BLUE)
).toBeLessThan(3)
await page
.getByRole('button', {
name: 'Length: open menu',
})
.click()
await page.getByRole('button', { name: 'Horizontal', exact: true }).click()
let activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(`|> xLine(3.13, %)`)
// If the overlay-angle is updated the THREE.js scene is in a good state
await expect(
await page.locator('[data-overlay-index="1"]')
).toHaveAttribute('data-overlay-angle', '0')
const lineAfter = await u.getSegmentBodyCoords(
`[data-overlay-index="1"]`,
0
)
expect(
await u.getGreatestPixDiff(lineAfter, TEST_COLORS.BLUE)
).toBeLessThan(3)
await page.waitForTimeout(300)
await page
.getByRole('button', {
name: 'Length: open menu',
})
.click()
// await expect(page.getByRole('button', { name: 'length', exact: true })).toBeVisible()
await page.waitForTimeout(200)
// await page.getByRole('button', { name: 'length', exact: true }).click()
await page.getByTestId('dropdown-constraint-length').click()
await page.getByLabel('length Value').fill('10')
await page.getByRole('button', { name: 'Add constraining value' }).click()
activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
})
})

View File

@ -0,0 +1,249 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import { uuidv4 } from 'lib/utils'
import { TEST_CODE_GIZMO } from './storageStates'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Testing Gizmo', () => {
const cases = [
{
testDescription: 'top view',
clickPosition: { x: 951, y: 385 },
expectedCameraPosition: { x: 800, y: -152, z: 4886.02 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
},
{
testDescription: 'bottom view',
clickPosition: { x: 951, y: 429 },
expectedCameraPosition: { x: 800, y: -152, z: -4834.02 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
},
{
testDescription: 'right view',
clickPosition: { x: 929, y: 417 },
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
},
{
testDescription: 'left view',
clickPosition: { x: 974, y: 397 },
expectedCameraPosition: { x: -4060.02, y: -152, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
},
{
testDescription: 'back view',
clickPosition: { x: 967, y: 421 },
expectedCameraPosition: { x: 800, y: 4708.02, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
},
{
testDescription: 'front view',
clickPosition: { x: 935, y: 393 },
expectedCameraPosition: { x: 800, y: -5012.02, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
},
] as const
for (const {
clickPosition,
expectedCameraPosition,
expectedCameraTarget,
testDescription,
} of cases) {
test(`check ${testDescription}`, async ({ page, browserName }) => {
const u = await getUtils(page)
await page.addInitScript((TEST_CODE_GIZMO) => {
localStorage.setItem('persistCode', TEST_CODE_GIZMO)
}, TEST_CODE_GIZMO)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await page.waitForTimeout(100)
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: {
x: 3000,
y: 3000,
z: 3000,
},
center: {
x: 800,
y: -152,
z: 26,
},
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.clearCommandLogs()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await u.waitForCmdReceive('default_camera_get_settings')
await u.clearCommandLogs()
await page.mouse.move(clickPosition.x, clickPosition.y)
await page.waitForTimeout(100)
await page.mouse.click(clickPosition.x, clickPosition.y)
await page.mouse.move(0, 0)
await u.waitForCmdReceive('default_camera_look_at')
await u.clearCommandLogs()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await u.waitForCmdReceive('default_camera_get_settings')
await Promise.all([
// position
expect(page.getByTestId('cam-x-position')).toHaveValue(
expectedCameraPosition.x.toString()
),
expect(page.getByTestId('cam-y-position')).toHaveValue(
expectedCameraPosition.y.toString()
),
expect(page.getByTestId('cam-z-position')).toHaveValue(
expectedCameraPosition.z.toString()
),
// target
expect(page.getByTestId('cam-x-target')).toHaveValue(
expectedCameraTarget.x.toString()
),
expect(page.getByTestId('cam-y-target')).toHaveValue(
expectedCameraTarget.y.toString()
),
expect(page.getByTestId('cam-z-target')).toHaveValue(
expectedCameraTarget.z.toString()
),
])
})
}
test('Context menu and popover menu', async ({ page }) => {
const testCase = {
testDescription: 'Right view',
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
}
// Test prelude taken from the above test
const u = await getUtils(page)
await page.addInitScript((TEST_CODE_GIZMO) => {
localStorage.setItem('persistCode', TEST_CODE_GIZMO)
}, TEST_CODE_GIZMO)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await page.waitForTimeout(100)
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: {
x: 3000,
y: 3000,
z: 3000,
},
center: {
x: 800,
y: -152,
z: 26,
},
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.clearCommandLogs()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await u.waitForCmdReceive('default_camera_get_settings')
// Now find and select the correct
// view from the context menu
await u.clearCommandLogs()
const gizmo = page.locator('[aria-label*=gizmo]')
await gizmo.click({ button: 'right' })
const buttonToTest = page.getByRole('button', {
name: testCase.testDescription,
})
await expect(buttonToTest).toBeVisible()
await buttonToTest.click()
// Now assert we've moved to the correct view
// Taken from the above test
await u.waitForCmdReceive('default_camera_look_at')
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await u.waitForCmdReceive('default_camera_get_settings')
await page.waitForTimeout(400)
await Promise.all([
// position
expect(page.getByTestId('cam-x-position')).toHaveValue(
testCase.expectedCameraPosition.x.toString()
),
expect(page.getByTestId('cam-y-position')).toHaveValue(
testCase.expectedCameraPosition.y.toString()
),
expect(page.getByTestId('cam-z-position')).toHaveValue(
testCase.expectedCameraPosition.z.toString()
),
// target
expect(page.getByTestId('cam-x-target')).toHaveValue(
testCase.expectedCameraTarget.x.toString()
),
expect(page.getByTestId('cam-y-target')).toHaveValue(
testCase.expectedCameraTarget.y.toString()
),
expect(page.getByTestId('cam-z-target')).toHaveValue(
testCase.expectedCameraTarget.z.toString()
),
])
// Now test the popover menu.
// It has the same click handlers, so we can just
// test that it opens and contains the same content.
const gizmoPopoverButton = page.getByRole('button', {
name: 'view settings',
})
await expect(gizmoPopoverButton).toBeVisible()
await gizmoPopoverButton.click()
await expect(buttonToTest).toBeVisible()
})
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,231 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates'
import * as TOML from '@iarna/toml'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Testing settings', () => {
test('Stored settings are validated and fall back to defaults', async ({
page,
}) => {
const u = await getUtils(page)
// Override beforeEach test setup
// with corrupted settings
await page.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: TEST_SETTINGS_CORRUPTED }),
}
)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// Check the settings were reset
const storedSettings = TOML.parse(
await page.evaluate(
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
{ settingsKey: TEST_SETTINGS_KEY }
)
) as { settings: SaveSettingsPayload }
expect(storedSettings.settings?.app?.theme).toBe(undefined)
// Check that the invalid settings were removed
expect(storedSettings.settings?.modeling?.defaultUnit).toBe(undefined)
expect(storedSettings.settings?.modeling?.mouseControls).toBe(undefined)
expect(storedSettings.settings?.app?.projectDirectory).toBe(undefined)
expect(storedSettings.settings?.projects?.defaultProjectName).toBe(
undefined
)
})
test('Project settings can be set and override user settings', async ({
page,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
// Open the settings modal with the browser keyboard shortcut
await page.keyboard.press('Meta+Shift+,')
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await page
.locator('select[name="app-theme"]')
.selectOption({ value: 'light' })
// Verify the toast appeared
await expect(
page.getByText(`Set theme to "light" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
// Check that the user setting was not changed
await page.getByRole('radio', { name: 'User' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('dark')
// Roll back to default "system" theme
await page
.getByText(
'themeRoll back themeRoll back to match defaultThe overall appearance of the appl'
)
.hover()
await page
.getByRole('button', {
name: 'Roll back theme',
})
.click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Check that the project setting did not change
await page.getByRole('radio', { name: 'Project' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
})
test('Project settings can be opened with keybinding from the editor', async ({
page,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
// Put the cursor in the editor
await page.locator('.cm-content').click()
// Open the settings modal with the browser keyboard shortcut
await page.keyboard.press('Meta+Shift+,')
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await page
.locator('select[name="app-theme"]')
.selectOption({ value: 'light' })
// Verify the toast appeared
await expect(
page.getByText(`Set theme to "light" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
// Check that the user setting was not changed
await page.getByRole('radio', { name: 'User' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('dark')
// Roll back to default "system" theme
await page
.getByText(
'themeRoll back themeRoll back to match defaultThe overall appearance of the appl'
)
.hover()
await page
.getByRole('button', {
name: 'Roll back theme',
})
.click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Check that the project setting did not change
await page.getByRole('radio', { name: 'Project' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
})
test('Project and user settings can be reset', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
// Put the cursor in the editor
await page.locator('.cm-content').click()
// Open the settings modal with the browser keyboard shortcut
await page.keyboard.press('Meta+Shift+,')
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
// Click the reset settings button.
await page.getByRole('button', { name: 'Restore default settings' }).click()
await page
.locator('select[name="app-theme"]')
.selectOption({ value: 'light' })
// Verify the toast appeared
await expect(
page.getByText(`Set theme to "light" for this project`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
// Check that the user setting was not changed
await page.getByRole('radio', { name: 'User' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Click the reset settings button.
await page.getByRole('button', { name: 'Restore default settings' }).click()
// Verify it is now set to the default value
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Set the user theme to light.
await page
.locator('select[name="app-theme"]')
.selectOption({ value: 'light' })
// Verify the toast appeared
await expect(
page.getByText(`Set theme to "light" as a user default`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
await page.getByRole('radio', { name: 'Project' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
// Click the reset settings button.
await page.getByRole('button', { name: 'Restore default settings' }).click()
// Verify it is now set to the default value
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
await page.getByRole('radio', { name: 'User' }).click()
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
// Click the reset settings button.
await page.getByRole('button', { name: 'Restore default settings' }).click()
// Verify it is now set to the default value
await expect(page.locator('select[name="app-theme"]')).toHaveValue('system')
})
})

View File

@ -0,0 +1,569 @@
import { test, expect } from '@playwright/test'
import {
doExport,
getUtils,
makeTemplate,
metaModifier,
setup,
tearDown,
} from './test-utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test('Units menu', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
await u.waitForAuthSkipAppStart()
const unitsMenuButton = page.getByRole('button', {
name: 'Current Units',
exact: false,
})
await expect(unitsMenuButton).toBeVisible()
await expect(unitsMenuButton).toContainText('in')
await unitsMenuButton.click()
const millimetersButton = page.getByRole('button', { name: 'Millimeters' })
await expect(millimetersButton).toBeVisible()
await millimetersButton.click()
// Look out for the toast message
const toastMessage = page.getByText(
`Set default unit to "mm" for this project`
)
await expect(toastMessage).toBeVisible()
// Verify that the popover has closed
await expect(millimetersButton).not.toBeAttached()
// Verify that the button label has updated
await expect(unitsMenuButton).toContainText('mm')
})
test('Successful export shows a success toast', async ({ page }) => {
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = await getUtils(page)
await page.addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
localStorage.setItem(
'persistCode',
`const topAng = 25
const bottomAng = 35
const baseLen = 3.5
const baseHeight = 1
const totalHeightHalf = 2
const armThick = 0.5
const totalLen = 9.5
const part001 = startSketchOn('-XZ')
|> startProfileAt([0, 0], %)
|> yLine(baseHeight, %)
|> xLine(baseLen, %)
|> angledLineToY({
angle: topAng,
to: totalHeightHalf,
}, %, $seg04)
|> xLineTo(totalLen, %, $seg03)
|> yLine(-armThick, %, $seg01)
|> angledLineThatIntersects({
angle: HALF_TURN,
offset: -armThick,
intersectTag: seg04
}, %)
|> angledLineToY([segAng(seg04) + 180, ZERO], %)
|> angledLineToY({
angle: -bottomAng,
to: -totalHeightHalf - armThick,
}, %, $seg02)
|> xLineTo(segEndX(seg03) + 0, %)
|> yLine(-segLen(seg01), %)
|> angledLineThatIntersects({
angle: HALF_TURN,
offset: -armThick,
intersectTag: seg02
}, %)
|> angledLineToY([segAng(seg02) + 180, -baseHeight], %)
|> xLineTo(ZERO, %)
|> close(%)
|> extrude(4, %)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.waitForCmdReceive('extrude')
await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel()
await doExport(
{
type: 'gltf',
storage: 'embedded',
presentation: 'pretty',
},
page
)
// This is the main thing we're testing,
// We test the export functionality across all
// file types in snapshot-tests.spec.ts
await expect(page.getByText('Exported successfully')).toBeVisible()
})
test('Paste should not work unless an input is focused', async ({
page,
browserName,
}) => {
// To run this test locally, uncomment Firefox in playwright.config.ts
test.skip(
browserName !== 'firefox',
"This bug is really Firefox-only, which we don't run in CI."
)
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
const codeEditorText = page.locator('.cm-content')
const pasteContent = `// was this pasted?`
const typeContent = `// this should be typed`
// Load text into the clipboard
await page.evaluate((t) => navigator.clipboard.writeText(t), pasteContent)
// Focus the text editor
await codeEditorText.focus()
// Show that we can type into it
await page.keyboard.type(typeContent)
await page.keyboard.press('Enter')
// Paste without the code pane focused
await codeEditorText.blur()
await page.keyboard.press(`${metaModifier}+KeyV`)
// Show that the paste didn't work but typing did
await expect(codeEditorText).not.toContainText(pasteContent)
await expect(codeEditorText).toContainText(typeContent)
// Paste with the code editor focused
// Following this guidance: https://github.com/microsoft/playwright/issues/8114
await codeEditorText.focus()
await page.keyboard.press(`${metaModifier}+KeyV`)
await expect(
await page.evaluate(
() => document.querySelector('.cm-content')?.textContent
)
).toContain(pasteContent)
})
test('Keyboard shortcuts can be viewed through the help menu', async ({
page,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
await page
.getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
// Open the help menu
await page.getByRole('button', { name: 'Help and resources' }).click()
// Open the keyboard shortcuts
await page.getByRole('button', { name: 'Keyboard Shortcuts' }).click()
// Verify the URL and that you can see a list of shortcuts
await expect(page.url()).toContain('?tab=keybindings')
await expect(
page.getByRole('heading', { name: 'Enter Sketch Mode' })
).toBeAttached()
})
test('First escape in tool pops you out of tool, second exits sketch mode', async ({
page,
}) => {
// Wait for the app to be ready for use
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const lineButton = page.getByRole('button', { name: 'Line', exact: true })
const arcButton = page.getByRole('button', {
name: 'Tangential Arc',
exact: true,
})
// Test these hotkeys perform actions when
// focus is on the canvas
await page.mouse.move(600, 250)
await page.mouse.click(600, 250)
// Start a sketch
await page.keyboard.press('s')
await page.mouse.move(800, 300)
await page.mouse.click(800, 300)
await page.waitForTimeout(1000)
await expect(lineButton).toBeVisible()
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
// Draw a line
await page.mouse.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200)
await page.mouse.move(800, 250, { steps: 5 })
await page.mouse.click(800, 250)
// Unequip line tool
await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode.
await expect(page.getByRole('button', { name: 'Exit Sketch' })).toBeVisible()
await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true')
// Equip arc tool
await page.keyboard.press('a')
await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
await page.mouse.move(1000, 100, { steps: 5 })
await page.mouse.click(1000, 100)
await page.keyboard.press('Escape')
await page.keyboard.press('l')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
// Do not close the sketch.
// On close it will exit sketch mode.
// Unequip line tool
await page.keyboard.press('Escape')
await expect(lineButton).toHaveAttribute('aria-pressed', 'false')
await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
// Make sure we didn't pop out of sketch mode.
await expect(page.getByRole('button', { name: 'Exit Sketch' })).toBeVisible()
// Exit sketch
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
})
test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
// This test can run long if it takes a little too long to load
// the engine.
test.setTimeout(90000)
// This test has a weird bug on ubuntu
test.skip(
process.platform === 'linux',
'weird playwright bug on ubuntu https://github.com/KittyCAD/modeling-app/issues/2444'
)
// Load the app with the code pane open
await page.addInitScript(async () => {
localStorage.setItem(
'store',
JSON.stringify({
state: {
openPanes: ['code'],
},
version: 0,
})
)
})
// Wait for the app to be ready for use
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const codePane = page.getByRole('textbox').locator('div')
const codePaneButton = page.getByTestId('code-pane-button')
const lineButton = page.getByRole('button', { name: 'Line', exact: true })
const arcButton = page.getByRole('button', {
name: 'Tangential Arc',
exact: true,
})
const extrudeButton = page.getByRole('button', { name: 'Extrude' })
// Test that the hotkeys do nothing when
// focus is on the code pane
await codePane.click()
await page.keyboard.press('/')
await page.keyboard.press('/')
await page.keyboard.press('s')
await page.keyboard.press('l')
await page.keyboard.press('a')
await page.keyboard.press('e')
await expect(page.locator('.cm-content')).toHaveText('//slae')
await page.keyboard.press('Meta+/')
await page.waitForTimeout(1000)
// Test these hotkeys perform actions when
// focus is on the canvas
await page.mouse.move(600, 250)
await page.mouse.click(600, 250)
// work-around: to stop "keyboard.press('s')" from typing in the editor even when it should be blurred
await page.getByRole('button', { name: 'Commands' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Escape')
await page.waitForTimeout(100)
// end work-around
// Start a sketch
await page.keyboard.press('s')
await page.waitForTimeout(1000)
await page.mouse.move(800, 300, { steps: 5 })
await page.mouse.click(800, 300)
await page.waitForTimeout(1000)
await expect(lineButton).toHaveAttribute('aria-pressed', 'true', {
timeout: 15_000,
})
/**
* TODO: There is a bug somewhere that causes this test to fail
* if you toggle the codePane closed before your trigger the
* start of the sketch.
* and a separate Safari-only bug that causes the test to fail
* if the pane is open the entire test. The maintainer of CodeMirror
* has pinpointed this to the unusual browser behavior:
* https://discuss.codemirror.net/t/how-to-force-unfocus-of-the-codemirror-element-in-safari/8095/3
*/
await codePaneButton.click()
await expect(u.codeLocator).not.toBeVisible()
await page.waitForTimeout(300)
// Draw a line
await page.mouse.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200)
await page.waitForTimeout(300)
await page.mouse.move(800, 250, { steps: 5 })
await page.mouse.click(800, 250)
// Unequip line tool
await page.keyboard.press('l')
await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true')
// Equip arc tool
await page.keyboard.press('a')
await expect(arcButton).toHaveAttribute('aria-pressed', 'true', {
timeout: 10_000,
})
await page.mouse.move(1000, 100, { steps: 5 })
await page.mouse.click(1000, 100)
await page.keyboard.press('Escape')
await page.keyboard.press('l')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
// Close profile
await page.mouse.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200)
// On close it will unequip the line tool.
await expect(lineButton).toHaveAttribute('aria-pressed', 'false')
// Exit sketch
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
await page.waitForTimeout(400)
// Extrude
await page.mouse.click(750, 150)
await expect(extrudeButton).not.toBeDisabled()
await page.keyboard.press('e')
await page.waitForTimeout(100)
await page.mouse.move(800, 200, { steps: 5 })
await page.mouse.click(800, 200)
await page.waitForTimeout(300)
await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible()
await page.getByRole('button', { name: 'Continue' }).click()
await page.waitForTimeout(300)
await expect(
page.getByRole('button', { name: 'Submit command' })
).toBeVisible()
await page.getByRole('button', { name: 'Submit command' }).click()
await codePaneButton.click()
await expect(page.locator('.cm-content')).toContainText('extrude(')
})
test('Delete key does not navigate back', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
const settingsButton = page.getByRole('link', {
name: 'Settings',
exact: false,
})
const settingsCloseButton = page.getByTestId('settings-close-button')
await settingsButton.click()
await expect(page.url()).toContain('/settings')
// Make sure that delete doesn't go back from settings
await page.keyboard.press('Delete')
await expect(page.url()).toContain('/settings')
// Now close the settings and try delete again,
// make sure it doesn't go back to settings
await settingsCloseButton.click()
await page.keyboard.press('Delete')
await expect(page.url()).not.toContain('/settings')
})
test('Sketch on face', async ({ page }) => {
test.setTimeout(90_000)
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([3.29, 7.86], %)
|> line([2.48, 2.44], %)
|> line([2.66, 1.17], %)
|> line([3.75, 0.46], %)
|> line([4.99, -0.46], %)
|> line([3.3, -2.12], %)
|> line([2.16, -3.33], %)
|> line([0.85, -3.08], %)
|> line([-0.18, -3.36], %)
|> line([-3.86, -2.73], %)
|> line([-17.67, 0.85], %)
|> close(%)
const extrude001 = extrude(5 + 7, sketch001)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(300)
let previousCodeContent = await page.locator('.cm-content').innerText()
await u.openAndClearDebugPanel()
await u.doAndWaitForCmd(
() => page.mouse.click(625, 133),
'default_camera_get_settings',
true
)
await page.waitForTimeout(150)
await u.closeDebugPanel()
const firstClickPosition = [612, 238]
const secondClickPosition = [661, 242]
const thirdClickPosition = [609, 267]
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(secondClickPosition[0], secondClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
await expect(page.locator('.cm-content'))
.toContainText(`const sketch002 = startSketchOn(extrude001, seg01)
|> startProfileAt([-12.94, 6.6], %)
|> line([2.45, -0.2], %)
|> line([-2.6, -1.25], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.updateCamPosition([1049, 239, 686])
await u.closeDebugPanel()
await page.getByText('startProfileAt([-12.94, 6.6], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
await page.waitForTimeout(150)
await page.setViewportSize({ width: 1200, height: 1200 })
await u.openAndClearDebugPanel()
await u.updateCamPosition([452, -152, 1166])
await u.closeDebugPanel()
await page.waitForTimeout(200)
const pointToDragFirst = [787, 565]
await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1])
await page.mouse.down()
await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], {
steps: 5,
})
await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText()
const result = makeTemplate`const sketch002 = startSketchOn(extrude001, seg01)
|> startProfileAt([-12.83, 6.7], %)
|> line([${[2.28, 2.35]}, -${0.07}], %)
|> line([-3.05, -1.47], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
await expect(page.locator('.cm-content')).toHaveText(result.regExp)
// exit sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await page.getByText('startProfileAt([-12.94, 6.6], %)').click()
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Extrude' }).click()
await expect(page.getByTestId('command-bar')).toBeVisible()
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await expect(page.getByText('Confirm Extrude')).toBeVisible()
await page.keyboard.press('Enter')
const result2 = result.genNext`
const sketch002 = extrude(${[5, 5]} + 7, sketch002)`
await expect(page.locator('.cm-content')).toHaveText(result2.regExp)
})

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