Compare commits

..

1 Commits

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

View File

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

View File

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

View File

@ -165,6 +165,7 @@ jobs:
- name: Build the app (release)
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
env:
PUBLISH_FOR_PULL_REQUEST: true
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
@ -172,6 +173,7 @@ jobs:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always
@ -227,6 +229,7 @@ jobs:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always
@ -359,17 +362,6 @@ jobs:
- name: List artifacts
run: "ls -R out"
- name: Set more complete nightly release notes
if: ${{ env.IS_NIGHTLY == 'true' }}
run: |
# Note: preferred going this way instead of a full clone in the checkout step,
# see https://github.com/actions/checkout/issues/1471
git fetch --prune --unshallow --tags
export TAG="nightly-${VERSION}"
export PREVIOUS_TAG=$(git describe --tags --match="nightly-v[0-9]*" --abbrev=0)
export NOTES=$(./scripts/get-nightly-changelog.sh)
yarn files:set-notes
- name: Authenticate to Google Cloud
if: ${{ env.IS_NIGHTLY == 'true' }}
uses: 'google-github-actions/auth@v2.1.7'
@ -391,17 +383,12 @@ jobs:
parent: false
destination: 'dl.kittycad.io/releases/modeling-app/nightly'
- name: Invalidate bucket cache on latest*.yml and last_download.json files
if: ${{ env.IS_NIGHTLY == 'true' }}
run: yarn files:invalidate-bucket:nightly
- name: Tag nightly commit
if: ${{ env.IS_NIGHTLY == 'true' }}
uses: actions/github-script@v7
- name: Create draft release
uses: softprops/action-gh-release@v2
if: ${{ env.IS_RELEASE == 'true' }}
with:
script: |
const { VERSION } = process.env
const { owner, repo } = context.repo
const { sha } = context
const ref = `refs/tags/nightly-${VERSION}`
github.rest.git.createRef({ owner, repo, sha, ref })
name: ${{ env.VERSION }}
tag_name: ${{ env.VERSION }}
draft: true
generate_release_notes: true
files: 'out/Zoo*'

View File

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

View File

@ -126,13 +126,11 @@ jobs:
destination: 'dl.kittycad.io/releases/modeling-app'
- name: Invalidate bucket cache on latest*.yml and last_download.json files
run: yarn files:invalidate-bucket
- name: Upload release files to Github
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2
with:
files: 'out/Zoo*'
run: |
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/last_download.json" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-linux-arm64.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async
announce_release:

1
.gitignore vendored
View File

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

2
.nvmrc
View File

@ -1 +1 @@
v22.12.0
v21.7.3

View File

@ -1,43 +0,0 @@
# Setting Up Zoo Modeling App
Compared to other CAD software, getting Zoo Modeling App up and running is quick and straightforward across platforms. It's about 100MB to download and is quick to install.
## Windows
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for Windows and for your processor type.
2. Once downloaded, run the installer `Zoo Modeling App-{version}-{arch}-win.exe` which should take a few seconds.
3. The installation happens at `C:\Program Files\Zoo Modeling App`. A shortcut in the start menu is also created so you can run the app easily by clicking on it.
## macOS
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for macOS and for your processor type.
2. Once downloaded, open the disk image `Zoo Modeling App-{version}-{arch}-mac.dmg` and drag the applications to your `Applications` directory.
3. You can then open your `Applications` directory and double-click on `Zoo Modeling App` to open.
## Linux
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for Linux and for your processor type.
2. Install the dependencies needed to run the [AppImage format](https://appimage.org/).
- On Ubuntu, install the FUSE library with these commands in a terminal.
```bash
sudo apt update
sudo apt install libfuse2
```
- Optionally, follow [these steps](https://github.com/probonopd/go-appimage/blob/master/src/appimaged/README.md#initial-setup) to install `appimaged`. It is a daemon that makes interacting with AppImage files more seamless.
- Once installed, copy the downloaded `Zoo Modeling App-{version}-{arch}-linux.AppImage` to the directory of your choice, for instance `~/Applications`.
- `appimaged` should automatically find it and make it executable. If not, run:
```bash
chmod a+x ~/Applications/Zoo\ Modeling\ App-{version}-{arch}-linux.AppImage
```
3. You can double-click on the AppImage to run it, or in a terminal with this command:
```bash
~/Applications/Zoo\ Modeling\ App-{version}-{arch}-linux.AppImage
```

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -22,5 +22,3 @@ once fixed in engine will just start working here with no language changes.
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work.

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,6 @@ layout: manual
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
* [`angledLineToX`](kcl/angledLineToX)
* [`angledLineToY`](kcl/angledLineToY)
* [`appearance`](kcl/appearance)
* [`arc`](kcl/arc)
* [`arcTo`](kcl/arcTo)
* [`asin`](kcl/asin)

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,23 +0,0 @@
---
title: "AppearanceData"
excerpt: "Data for appearance."
layout: manual
---
Data for appearance.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `color` |`string`| Color of the new material, a hex string like "#ff0000". | No |
| `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No |
| `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No |

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,27 @@
import { test, expect, Page } from './zoo-test'
import { HomePageFixture } from './fixtures/homePageFixture'
import { getUtils } from './test-utils'
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 }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Can create sketches on all planes and their back sides', () => {
const sketchOnPlaneAndBackSideTest = async (
page: Page,
homePage: HomePageFixture,
page: any,
plane: string,
clickCoords: { x: number; y: number }
) => {
const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
const coord =
@ -77,39 +83,32 @@ test.describe('Can create sketches on all planes and their back sides', () => {
await u.clearCommandLogs()
await u.removeCurrentCode()
}
test('XY', async ({ page, homePage }) => {
test('XY', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(
page,
homePage,
'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, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, 'YZ', { x: 700, y: 250 }) // green plane
test('YZ', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, 'YZ', { x: 700, y: 250 }) // green plane
})
test('XZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, '-XZ', { x: 700, y: 80 }) // blue plane
test('XZ', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, '-XZ', { x: 700, y: 80 }) // blue plane
})
test('-XY', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, '-XY', {
x: 600,
y: 118,
}) // back of red plane
test('-XY', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, '-XY', { x: 600, y: 118 }) // back of red plane
})
test('-YZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, '-YZ', {
x: 700,
y: 219,
}) // back of green plan
test('-YZ', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, '-YZ', { x: 700, y: 219 }) // back of green plane
})
test('-XZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, homePage, 'XZ', { x: 700, y: 427 }) // back of blue plane
test('-XZ', async ({ page }) => {
await sketchOnPlaneAndBackSideTest(page, 'XZ', { x: 700, y: 427 }) // back of blue plane
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@ export class EditorFixture {
reConstruct = (page: Page) => {
this.page = page
this.codeContent = page.locator('.cm-content[data-language="kcl"]')
this.codeContent = page.locator('.cm-content')
this.diagnosticsTooltip = page.locator('.cm-tooltip-lint')
this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error')
this.activeLine = this.page.locator('.cm-activeLine')
@ -54,13 +54,13 @@ export class EditorFixture {
}
}
if (!shouldNormalise) {
const expectStart = expect.poll(() => this.codeContent.textContent())
const expectStart = expect(this.codeContent)
if (not) {
const result = await expectStart.not.toContain(code)
const result = await expectStart.not.toContainText(code, { timeout })
await resetPane()
return result
}
const result = await expectStart.toContain(code)
const result = await expectStart.toContainText(code, { timeout })
await resetPane()
return result
}
@ -147,20 +147,4 @@ export class EditorFixture {
openPane() {
return openPane(this.page, this.paneButtonTestId)
}
scrollToText(text: string) {
return this.page.evaluate((scrollToText: string) => {
// editorManager is available on the window object.
// @ts-ignore
let index = editorManager._editorView.docView.view.state.doc
.toString()
.indexOf(scrollToText)
// @ts-ignore
editorManager._editorView.dispatch({
selection: {
anchor: index,
},
scrollIntoView: true,
})
}, text)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,20 +1,27 @@
import { test, expect, Page } from './zoo-test'
import fs from 'node:fs/promises'
import path from 'node:path'
import { HomePageFixture } from './fixtures/homePageFixture'
import { test, expect, Page } from '@playwright/test'
import { test as test2, expect as expect2 } from './fixtures/fixtureSetup'
import {
getMovementUtils,
getUtils,
PERSIST_MODELING_CONTEXT,
setup,
tearDown,
} from './test-utils'
import { uuidv4, roundOff } from 'lib/utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Sketch tests', () => {
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
page,
context,
homePage,
}) => {
const u = await getUtils(page)
const selectionsSnippets = {
@ -28,53 +35,53 @@ test.describe('Sketch tests', () => {
localStorage.setItem(
'persistCode',
`
width = 20
height = 10
thickness = 5
screwRadius = 3
wireRadius = 2
wireOffset = 0.5
screwHole = startSketchOn('XY')
${startProfileAt1}
|> arc({
radius = screwRadius,
angle_start = 0,
angle_end = 360
}, %)
part001 = startSketchOn('XY')
${startProfileAt2}
|> xLine(width * .5, %)
|> yLine(height, %)
|> xLine(-width * .5, %)
|> close(%)
|> hole(screwHole, %)
|> extrude(thickness, %)
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, %)
`
width = 20
height = 10
thickness = 5
screwRadius = 3
wireRadius = 2
wireOffset = 0.5
screwHole = startSketchOn('XY')
${startProfileAt1}
|> arc({
radius = screwRadius,
angle_start = 0,
angle_end = 360
}, %)
part001 = startSketchOn('XY')
${startProfileAt2}
|> xLine(width * .5, %)
|> yLine(height, %)
|> xLine(-width * .5, %)
|> close(%)
|> hole(screwHole, %)
|> extrude(thickness, %)
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.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
@ -101,22 +108,24 @@ test.describe('Sketch tests', () => {
})
test('Can delete most of a sketch and the line tool will still work', async ({
page,
homePage,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> xLine(12.73, %)
|> tangentialArcTo([24.95, -5.38], %)`
|> startProfileAt([4.61, -14.01], %)
|> xLine(12.73, %)
|> tangentialArcTo([24.95, -5.38], %)`
)
})
await homePage.goToModelingScene()
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' })
@ -133,7 +142,8 @@ test.describe('Sketch tests', () => {
await page.keyboard.press('Home')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
await u.openDebugPanel()
await u.openAndClearDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(100)
@ -141,31 +151,26 @@ test.describe('Sketch tests', () => {
await page.waitForTimeout(100)
await expect(async () => {
await page.mouse.move(700, 200, { steps: 25 })
await page.mouse.click(700, 200)
await expect
.poll(u.crushKclCodeIntoOneLineAndThenMaybeSome, { timeout: 1000 })
.toBe(
`sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61,-14.01], %)
|> yLine(15.95, %)
`
.replaceAll(' ', '')
.replaceAll('\n', '')
)
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
.toBe(`sketch001 = startSketchOn('XZ')
|> startProfileAt([12.34, -12.34], %)
|> yLine(12.34, %)
`)
}).toPass({ timeout: 40_000, intervals: [1_000] })
})
test('Can exit selection of face', async ({ page, homePage }) => {
test('Can exit selection of face', async ({ page }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem('persistCode', ``)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect(
@ -182,7 +187,6 @@ test.describe('Sketch tests', () => {
test.describe('Can edit segments by dragging their handles', () => {
const doEditSegmentsByDraggingHandle = async (
page: Page,
homePage: HomePageFixture,
openPanes: string[]
) => {
// Load the app with the code panes
@ -198,8 +202,9 @@ test.describe('Sketch tests', () => {
})
const u = await getUtils(page)
await homePage.goToModelingScene()
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -313,7 +318,7 @@ test.describe('Sketch tests', () => {
|> line([1.97, 2.06], %)
|> close(%)`)
}
test('code pane open at start-handles', async ({ page, homePage }) => {
test('code pane open at start-handles', async ({ page }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
@ -326,10 +331,10 @@ test.describe('Sketch tests', () => {
})
)
})
await doEditSegmentsByDraggingHandle(page, homePage, ['code'])
await doEditSegmentsByDraggingHandle(page, ['code'])
})
test('code pane closed at start-handles', async ({ page, homePage }) => {
test('code pane closed at start-handles', async ({ page }) => {
// Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem(
@ -337,26 +342,25 @@ test.describe('Sketch tests', () => {
JSON.stringify({ openPanes: [] })
)
}, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(page, homePage, [])
await doEditSegmentsByDraggingHandle(page, [])
})
})
test('Can edit a circle center and radius by dragging its handles', async ({
page,
editor,
homePage,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
|> circle({ center = [4.61, -5.01], radius = 8 }, %)`
|> circle({ center = [4.61, -5.01], radius = 8 }, %)`
)
})
await homePage.goToModelingScene()
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -395,7 +399,6 @@ test.describe('Sketch tests', () => {
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText()
await expect(page.getByTestId('segment-overlay')).toHaveCount(1)
@ -406,9 +409,7 @@ test.describe('Sketch tests', () => {
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] - dragPX },
})
await page.waitForTimeout(100)
await editor.expectEditor.not.toContain(prevContent)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
})
@ -421,36 +422,35 @@ test.describe('Sketch tests', () => {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX * 2, y: lineEnd.y + dragPX },
})
await editor.expectEditor.not.toContain(prevContent)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
})
// expect the code to have changed
await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ')
|> circle({ center = [7.26, -2.37], radius = 11.44 }, %)`,
{ shouldNormalise: true }
)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> circle({ center = [7.26, -2.37], radius = 11.44 }, %)
`)
})
test('Can edit a sketch that has been extruded in the same pipe', async ({
page,
homePage,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -10.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -0.38], %)
|> close(%)
|> extrude(5, %)`
|> startProfileAt([4.61, -10.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -0.38], %)
|> close(%)
|> extrude(5, %)`
)
})
await homePage.goToModelingScene()
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -504,11 +504,11 @@ test.describe('Sketch tests', () => {
await page.waitForTimeout(100)
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 15, y: lineEnd.y },
targetPosition: { x: lineEnd.x, y: lineEnd.y + 15 },
})
await page.waitForTimeout(100)
await 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()
@ -517,8 +517,8 @@ test.describe('Sketch tests', () => {
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: tangentEnd.x + 10, y: tangentEnd.y - 5 },
targetPosition: {
x: tangentEnd.x,
y: tangentEnd.y - 15,
x: tangentEnd.x + dragPX,
y: tangentEnd.y + dragPX,
},
})
await page.waitForTimeout(100)
@ -527,33 +527,33 @@ test.describe('Sketch tests', () => {
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -12.68], %)
|> line([12.68, -1.09], %)
|> tangentialArcTo([24.89, 0.68], %)
|> close(%)
|> extrude(5, %)
`)
|> startProfileAt([7.12, -12.68], %)
|> line([15.39, -2.78], %)
|> tangentialArcTo([27.6, -3.05], %)
|> close(%)
|> extrude(5, %)
`)
})
test('Can edit a sketch that has been revolved in the same pipe', async ({
page,
homePage,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)
|> revolve({ axis = "X",}, %)`
|> startProfileAt([4.61, -14.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)
|> revolve({ axis = "X",}, %)`
)
})
await homePage.goToModelingScene()
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -629,20 +629,19 @@ test.describe('Sketch tests', () => {
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`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" }, %)`)
|> 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, homePage }) => {
test('Can add multiple sketches', async ({ page }) => {
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
await page.setBodyDimensions(viewportSize)
await page.setViewportSize(viewportSize)
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
@ -737,8 +736,9 @@ test.describe('Sketch tests', () => {
scale = 1
) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
const code = `sketch001 = startSketchOn('-XZ')
@ -820,39 +820,36 @@ test.describe('Sketch tests', () => {
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.removeCurrentCode()
}
test('[0, 100, 100]', async ({ page, homePage }) => {
await homePage.goToModelingScene()
test('[0, 100, 100]', async ({ page }) => {
await doSnapAtDifferentScales(page, [0, 100, 100], 0.01)
})
test('[0, 10000, 10000]', async ({ page, homePage }) => {
await homePage.goToModelingScene()
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,
homePage,
}) => {
// this was a regression https://github.com/KittyCAD/modeling-app/issues/2832
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`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(%)
`
|> 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.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
@ -888,30 +885,27 @@ test.describe('Sketch tests', () => {
timeout: 10_000,
})
})
test("Existing sketch with bad code delete user's code", async ({
page,
homePage,
}) => {
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',
`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(%)
extrude001 = extrude(5, sketch001)
`
|> 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(%)
extrude001 = extrude(5, sketch001)
`
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
@ -937,22 +931,26 @@ test.describe('Sketch tests', () => {
await expect((await u.codeLocator.innerText()).replace(/\s/g, '')).toBe(
`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(%)
extrude001 = extrude(5, sketch001)
sketch002 = startSketchOn(extrude001, 'END')
|>
`.replace(/\s/g, '')
|> 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(%)
extrude001 = extrude(5, sketch001)
sketch002 = startSketchOn(extrude001, 'END')
|>
`.replace(/\s/g, '')
)
})
test('empty-scene default-planes act as expected', async ({
page,
homePage,
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
@ -965,7 +963,9 @@ test.describe('Sketch tests', () => {
*/
const u = await getUtils(page)
await homePage.goToModelingScene()
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
@ -988,11 +988,11 @@ test.describe('Sketch tests', () => {
await u.openAndClearDebugPanel()
await u.codeLocator.fill(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> xLine(-20, %)
`)
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> xLine(-20, %)
`)
await u.expectCmdLog('[data-message-type="execution-done"]')
@ -1013,7 +1013,7 @@ test.describe('Sketch tests', () => {
// click start Sketch
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y, { steps: 50 })
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
@ -1027,19 +1027,21 @@ test.describe('Sketch tests', () => {
await page.waitForTimeout(200)
await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50)
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([11.8, 9.09], %)
|> line([3.39, -3.39], %)
`)
|> startProfileAt([11.8, 9.09], %)
|> line([3.39, -3.39], %)
`)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
|> startProfileAt([11.8, 9.09], %)
|> line([3.39, -3.39], %)
`
|> 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"]')
@ -1051,34 +1053,41 @@ test.describe('Sketch tests', () => {
).toBeLessThan(3)
})
test('Can attempt to sketch on revolved face', async ({ page, homePage }) => {
test('Can attempt to sketch on revolved face', async ({
page,
browserName,
}) => {
test.skip(
browserName === 'webkit',
'Skip on Safari until `window.tearDown` is working there'
)
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`lugHeadLength = 0.25
lugDiameter = 0.5
lugLength = 2
fn lug = (origin, length, diameter, plane) => {
lugSketch = startSketchOn(plane)
|> startProfileAt([origin[0] + lugDiameter / 2, origin[1]], %)
|> angledLineOfYLength({ angle = 60, length = lugHeadLength }, %)
|> xLineTo(0 + .001, %)
|> yLineTo(0, %)
|> close(%)
|> revolve({ axis = "Y" }, %)
return lugSketch
}
lug([0, 0], 10, .5, "XY")`
lugDiameter = 0.5
lugLength = 2
fn lug = (origin, length, diameter, plane) => {
lugSketch = startSketchOn(plane)
|> startProfileAt([origin[0] + lugDiameter / 2, origin[1]], %)
|> angledLineOfYLength({ angle = 60, length = lugHeadLength }, %)
|> xLineTo(0 + .001, %)
|> yLineTo(0, %)
|> close(%)
|> revolve({ axis = "Y" }, %)
return lugSketch
}
lug([0, 0], 10, .5, "XY")`
)
})
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
@ -1110,10 +1119,9 @@ test.describe('Sketch tests', () => {
test('Can sketch on face when user defined function was used in the sketch', async ({
page,
homePage,
}) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
// Checking for a regression that performs a sketch when a user defined function
// is declared at the top of the file and used in the sketch that is being drawn on.
@ -1123,51 +1131,51 @@ test.describe('Sketch tests', () => {
localStorage.setItem(
'persistCode',
`fn in2mm = (inches) => {
return inches * 25.4
}
const railTop = in2mm(.748)
const railSide = in2mm(.024)
const railBaseWidth = in2mm(.612)
const railWideWidth = in2mm(.835)
const railBaseLength = in2mm(.200)
const railClampable = in2mm(.200)
const rail = startSketchOn('XZ')
|> startProfileAt([
-railTop / 2,
railClampable + railBaseLength
], %)
|> lineTo([
railTop / 2,
railClampable + railBaseLength
], %)
|> lineTo([
railWideWidth / 2,
railClampable / 2 + railBaseLength
], %, $seg01)
|> lineTo([railTop / 2, railBaseLength], %)
|> lineTo([railBaseWidth / 2, railBaseLength], %)
|> lineTo([railBaseWidth / 2, 0], %)
|> lineTo([-railBaseWidth / 2, 0], %)
|> lineTo([-railBaseWidth / 2, railBaseLength], %)
|> lineTo([-railTop / 2, railBaseLength], %)
|> lineTo([
-railWideWidth / 2,
railClampable / 2 + railBaseLength
], %)
|> lineTo([
-railTop / 2,
railClampable + railBaseLength
], %)
|> close(%)
|> extrude(in2mm(2), %)`
return inches * 25.4
}
const railTop = in2mm(.748)
const railSide = in2mm(.024)
const railBaseWidth = in2mm(.612)
const railWideWidth = in2mm(.835)
const railBaseLength = in2mm(.200)
const railClampable = in2mm(.200)
const rail = startSketchOn('XZ')
|> startProfileAt([
-railTop / 2,
railClampable + railBaseLength
], %)
|> lineTo([
railTop / 2,
railClampable + railBaseLength
], %)
|> lineTo([
railWideWidth / 2,
railClampable / 2 + railBaseLength
], %, $seg01)
|> lineTo([railTop / 2, railBaseLength], %)
|> lineTo([railBaseWidth / 2, railBaseLength], %)
|> lineTo([railBaseWidth / 2, 0], %)
|> lineTo([-railBaseWidth / 2, 0], %)
|> lineTo([-railBaseWidth / 2, railBaseLength], %)
|> lineTo([-railTop / 2, railBaseLength], %)
|> lineTo([
-railWideWidth / 2,
railClampable / 2 + railBaseLength
], %)
|> lineTo([
-railTop / 2,
railClampable + railBaseLength
], %)
|> close(%)
|> extrude(in2mm(2), %)`
)
})
const center = { x: 600, y: 250 }
const rectangleSize = 20
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
// Start a sketch
await page.getByRole('button', { name: 'Start Sketch' }).click()
@ -1206,33 +1214,27 @@ test.describe('Sketch tests', () => {
})
})
test.describe('Sketch mode should be toleratant to syntax errors', () => {
test(
test2.describe('Sketch mode should be toleratant to syntax errors', () => {
test2(
'adding a syntax error, recovers after fixing',
{ tag: ['@skipWin'] },
async ({ page, homePage, context, scene, editor, toolbar }) => {
const file = await fs.readFile(
path.resolve(
__dirname,
'../../',
'./src/wasm-lib/tests/executor/inputs/e2e-can-sketch-on-chamfer.kcl'
),
'utf-8'
async ({ app, scene, editor, toolbar }) => {
test.skip(
process.platform === 'win32',
'a codemirror error appears in this test only on windows, that causes the test to fail only because of our "no new error" logic, but it can not be replicated locally'
)
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
const file = await app.getInputFile('e2e-can-sketch-on-chamfer.kcl')
await app.initialise(file)
const [objClick] = scene.makeMouseHelpers(600, 250)
const arrowHeadLocation = { x: 706, y: 129 } as const
const arrowHeadLocation = { x: 604, y: 129 } as const
const arrowHeadWhite: [number, number, number] = [255, 255, 255]
const backgroundGray: [number, number, number] = [28, 28, 28]
const verifyArrowHeadColor = async (c: [number, number, number]) =>
scene.expectPixelColor(c, arrowHeadLocation, 15)
await test.step('check chamfer selection changes cursor positon', async () => {
await expect(async () => {
await expect2(async () => {
// sometimes initial click doesn't register
await objClick()
await editor.expectActiveLinesToBe([
@ -1268,56 +1270,48 @@ test.describe('Sketch mode should be toleratant to syntax errors', () => {
// this checks sketch segments have been drawn
await verifyArrowHeadColor(arrowHeadWhite)
})
await page.waitForTimeout(100)
await app.page.waitForTimeout(100)
}
)
})
test.describe(`Sketching with offset planes`, () => {
test(`Can select an offset plane to sketch on`, async ({
context,
page,
scene,
toolbar,
editor,
homePage,
}) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
'persistCode',
`offsetPlane001 = offsetPlane("XY", 10)`
)
})
test2.describe(`Sketching with offset planes`, () => {
test2(
`Can select an offset plane to sketch on`,
async ({ app, scene, toolbar, editor }) => {
// We seed the scene with a single offset plane
await app.initialise(`offsetPlane001 = offsetPlane("XY", 10)`)
await homePage.goToModelingScene()
const [planeClick, planeHover] = scene.makeMouseHelpers(650, 200)
const [planeClick, planeHover] = scene.makeMouseHelpers(650, 200)
await test2.step(`Start sketching on the offset plane`, async () => {
await toolbar.startSketchPlaneSelection()
await test.step(`Start sketching on the offset plane`, async () => {
await toolbar.startSketchPlaneSelection()
await test.step(`Hovering should highlight code`, async () => {
await planeHover()
await editor.expectState({
activeLines: [`offsetPlane001=offsetPlane("XY",10)`],
diagnostics: [],
highlightedCode: 'offsetPlane("XY", 10)',
await test2.step(`Hovering should highlight code`, async () => {
await planeHover()
await editor.expectState({
activeLines: [`offsetPlane001=offsetPlane("XY",10)`],
diagnostics: [],
highlightedCode: 'offsetPlane("XY", 10)',
})
})
})
await test.step(`Clicking should select the plane and enter sketch mode`, async () => {
await planeClick()
// Have to wait for engine-side animation to finish
await page.waitForTimeout(600)
await expect(toolbar.lineBtn).toBeEnabled()
await editor.expectEditor.toContain('startSketchOn(offsetPlane001)')
await editor.expectState({
activeLines: [`offsetPlane001=offsetPlane("XY",10)`],
diagnostics: [],
highlightedCode: '',
})
await test2.step(
`Clicking should select the plane and enter sketch mode`,
async () => {
await planeClick()
// Have to wait for engine-side animation to finish
await app.page.waitForTimeout(600)
await expect2(toolbar.lineBtn).toBeEnabled()
await editor.expectEditor.toContain('startSketchOn(offsetPlane001)')
await editor.expectState({
activeLines: [`offsetPlane001=offsetPlane("XY",10)`],
diagnostics: [],
highlightedCode: '',
})
}
)
})
})
})
}
)
})

View File

@ -47,11 +47,7 @@ test.beforeEach(async ({ page }) => {
test.setTimeout(60_000)
// We test this end to end already - getting this to work on web just to take
// a snapshot of it feels weird. I'd rather our regular tests fail.
// The primary failure is doExport now relies on the filesystem. We can follow
// up with another PR if we want this back.
test.skip(
test(
'exports of each format should work',
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] },
async ({ page, context }) => {
@ -954,75 +950,7 @@ test(
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
// FIXME: Skip on macos its being weird.
// test.skip(process.platform === 'darwin', 'Skip on macos')
test('Grid turned off to on via command bar', async ({ page }) => {
const u = await getUtils(page)
const stream = page.getByTestId('stream')
const mask = [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
// wait for execution done
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(1)
await u.closeDebugPanel()
await u.closeKclCodePanel()
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(1000)
// Open the command bar.
await page
.getByRole('button', { name: 'Commands', exact: false })
.or(page.getByRole('button', { name: '⌘K' }))
.click()
const commandName = 'show scale grid'
const commandOption = page.getByRole('option', {
name: commandName,
exact: false,
})
const cmdSearchBar = page.getByPlaceholder('Search commands')
// This selector changes after we set the setting
await cmdSearchBar.fill(commandName)
await expect(commandOption).toBeVisible()
await commandOption.click()
const toggleInput = page.getByPlaceholder('Off')
await expect(toggleInput).toBeVisible()
await expect(toggleInput).toBeFocused()
// Select On
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute(
'data-headlessui-state',
'active selected'
)
await page.keyboard.press('ArrowUp')
await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set show scale grid to "true" as a user default`)
).toBeVisible()
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask,
})
})
test.skip(process.platform === 'darwin', 'Skip on macos')
test('Grid turned off', async ({ page }) => {
const u = await getUtils(page)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -109,21 +109,242 @@ keychain = startSketchOn("XY")
|> close(%)
|> extrude(thickness, %)
keychain1 = 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, %)
keychain2 = startSketchOn("XY")
|> startProfileAt([0, 0], %)
|> lineTo([width, 0], %)
|> lineTo([width, height], %)
|> lineTo([0, height], %)
|> close(%)
|> extrude(thickness, %)
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, %)
box = startSketchOn('XY')
|> startProfileAt([0, 0], %)
@ -133,7 +354,7 @@ box = startSketchOn('XY')
|> close(%)
|> extrude(10, %)
sketch001 = startSketchOn(box, revolveAxis)
sketch001 = startSketchOn(box, revolveAxis)
|> startProfileAt([5, 10], %)
|> line([0, -10], %)
|> line([2, 0], %)
@ -143,12 +364,18 @@ sketch001 = startSketchOn(box, revolveAxis)
axis: revolveAxis,
angle: 90
}, %)
return 0
}
sketch001 = startSketchOn('XZ')
|> startProfileAt([0.0, 0.0], %)
|> xLine(0.0, %)
|> close(%)
`
svg(startSketchOn(keychain, 'end'), [-33, 32], -thickness)
startSketchOn(keychain, 'end')
|> circle({ center: [
width / 2,
height - (keychainHoleSize + 1.5)
], radius: keychainHoleSize }, %)
|> extrude(-thickness, %)`
export const TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR = `thing = 1`

View File

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

View File

@ -1,20 +1,22 @@
import {
expect,
Page,
Download,
BrowserContext,
TestInfo,
_electron as electron,
Locator,
test,
} from '@playwright/test'
import { test, Page } from './zoo-test'
import { EngineCommand } from 'lang/std/artifactGraph'
import fsp from 'fs/promises'
import fsSync from 'fs'
import path from 'path'
import { join } from 'path'
import pixelMatch from 'pixelmatch'
import { PNG } from 'pngjs'
import { Protocol } from 'playwright-core/types/protocol'
import type { Models } from '@kittycad/lib'
import { COOKIE_NAME } from 'lib/constants'
import { APP_NAME, COOKIE_NAME } from 'lib/constants'
import { secrets } from './secrets'
import {
TEST_SETTINGS_KEY,
@ -28,134 +30,6 @@ import { isErrorWhitelisted } from './lib/console-error-whitelist'
import { isArray } from 'lib/utils'
import { reportRejection } from 'lib/trap'
// The below is copied from playwright-core because it exports none of them :(
import { Env, BrowserContextOptions } from 'playwright-core'
import type * as channels from '@protocol/channels'
// Copied from playwright-core
function envObjectToArray(env: Env): { name: string; value: string }[] {
const result: { name: string; value: string }[] = []
for (const name in env) {
if (!Object.is(env[name], undefined))
result.push({ name, value: String(env[name]) })
}
return result
}
// Copied from playwright-core
export async function toClientCertificatesProtocol(
certs?: BrowserContextOptions['clientCertificates']
): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
if (!certs) return undefined
const bufferizeContent = async (
value?: Buffer,
path?: string
): Promise<Buffer | undefined> => {
if (value) return value
if (path) return await fs.promises.readFile(path)
}
return await Promise.all(
certs.map(async (cert) => ({
origin: cert.origin,
cert: await bufferizeContent(cert.cert, cert.certPath),
key: await bufferizeContent(cert.key, cert.keyPath),
pfx: await bufferizeContent(cert.pfx, cert.pfxPath),
passphrase: cert.passphrase,
}))
)
}
// Copied from playwright-core
function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
if (acceptDownloads === undefined) return undefined
if (acceptDownloads) return 'accept'
return 'deny'
}
// Copied from playwright-core
function prepareRecordHarOptions(
options: BrowserContextOptions['recordHar']
): channels.RecordHarOptions | undefined {
if (!options) return
return {
path: options.path,
content: options.content || (options.omitContent ? 'omit' : undefined),
urlGlob: isString(options.urlFilter) ? options.urlFilter : undefined,
urlRegexSource: isRegExp(options.urlFilter)
? options.urlFilter.source
: undefined,
urlRegexFlags: isRegExp(options.urlFilter)
? options.urlFilter.flags
: undefined,
mode: options.mode,
}
}
// Copied from playwright-core
async function prepareStorageState(
options: BrowserContextOptions
): Promise<channels.BrowserNewContextParams['storageState']> {
if (typeof options.storageState !== 'string') return options.storageState
try {
return JSON.parse(await fs.promises.readFile(options.storageState, 'utf8'))
} catch (e) {
rewriteErrorMessage(
e,
`Error reading storage state from ${options.storageState}:\n` + e.message
)
throw e
}
}
// Copied from playwright-core
async function prepareBrowserContextParams(
options: BrowserContextOptions
): Promise<channels.BrowserNewContextParams> {
if (options.videoSize && !options.videosPath)
throw new Error(`"videoSize" option requires "videosPath" to be specified`)
if (options.extraHTTPHeaders)
network.validateHeaders(options.extraHTTPHeaders)
const contextParams: channels.BrowserNewContextParams = {
...options,
viewport: options.viewport === null ? undefined : options.viewport,
noDefaultViewport: options.viewport === null,
extraHTTPHeaders: options.extraHTTPHeaders
? headersObjectToArray(options.extraHTTPHeaders)
: undefined,
storageState: await prepareStorageState(options),
serviceWorkers: options.serviceWorkers,
recordHar: prepareRecordHarOptions(options.recordHar),
colorScheme:
options.colorScheme === null ? 'no-override' : options.colorScheme,
reducedMotion:
options.reducedMotion === null ? 'no-override' : options.reducedMotion,
forcedColors:
options.forcedColors === null ? 'no-override' : options.forcedColors,
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
clientCertificates: await toClientCertificatesProtocol(
options.clientCertificates
),
}
if (!contextParams.recordVideo && options.videosPath) {
contextParams.recordVideo = {
dir: options.videosPath,
size: options.videoSize,
}
}
if (contextParams.recordVideo && contextParams.recordVideo.dir)
contextParams.recordVideo.dir = path.resolve(
process.cwd(),
contextParams.recordVideo.dir
)
return contextParams
}
const toNormalizedCode = (text: string) => {
return text.replace(/\s+/g, '')
}
type TestColor = [number, number, number]
export const TEST_COLORS = {
WHITE: [249, 249, 249] as TestColor,
@ -224,16 +98,11 @@ async function removeCurrentCode(page: Page) {
}
export async function sendCustomCmd(page: Page, cmd: EngineCommand) {
const json = JSON.stringify(cmd)
await page.getByTestId('custom-cmd-input').fill(json)
await expect(page.getByTestId('custom-cmd-input')).toHaveValue(json)
await page.getByTestId('custom-cmd-send-button').scrollIntoViewIfNeeded()
await page.getByTestId('custom-cmd-input').fill(JSON.stringify(cmd))
await page.getByTestId('custom-cmd-send-button').click()
}
async function clearCommandLogs(page: Page) {
await page.getByTestId('custom-cmd-input').fill('')
await page.getByTestId('clear-commands').scrollIntoViewIfNeeded()
await page.getByTestId('clear-commands').click()
}
@ -281,19 +150,6 @@ export async function closePane(page: Page, testId: string) {
async function openKclCodePanel(page: Page) {
await openPane(page, 'code-pane-button')
// Code Mirror lazy loads text! Wowza! Let's force-load the text for tests.
await page.evaluate(() => {
// editorManager is available on the window object.
//@ts-ignore this is in an entirely different context that tsc can't see.
editorManager._editorView.dispatch({
selection: {
//@ts-ignore this is in an entirely different context that tsc can't see.
anchor: editorManager._editorView.docView.length,
},
scrollIntoView: true,
})
})
}
async function closeKclCodePanel(page: Page) {
@ -309,9 +165,6 @@ async function closeKclCodePanel(page: Page) {
async function openDebugPanel(page: Page) {
await openPane(page, 'debug-pane-button')
// The debug pane needs time to load everything.
await page.waitForTimeout(3000)
}
export async function closeDebugPanel(page: Page) {
@ -559,10 +412,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
.boundingBox({ timeout: 5_000 })
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
codeLocator: page.locator('.cm-content'),
crushKclCodeIntoOneLineAndThenMaybeSome: async () => {
const code = await page.locator('.cm-content').innerText()
return code.replaceAll(' ', '').replaceAll('\n', '')
},
normalisedEditorCode: async () => {
const code = await page.locator('.cm-content').innerText()
return normaliseKclNumbers(code)
@ -633,18 +482,13 @@ export async function getUtils(page: Page, test_?: typeof test) {
)
},
toNormalizedCode(text: string) {
return toNormalizedCode(text)
toNormalizedCode: (text: string) => {
return text.replace(/\s+/g, '')
},
async editorTextMatches(code: string) {
editorTextMatches: async (code: string) => {
const editor = page.locator(editorSelector)
return expect
.poll(async () => {
const text = await editor.textContent()
return toNormalizedCode(text ?? '')
})
.toContain(toNormalizedCode(code))
return expect(editor).toHaveText(code, { useInnerText: true })
},
pasteCodeInEditor: async (code: string) => {
@ -670,7 +514,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByTestId('create-file-button').click()
await page.getByTestId('tree-input-field').fill(name)
await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter')
})
},
@ -830,34 +674,6 @@ export const makeTemplate: (
}
}
const PLAYWRIGHT_DOWNLOAD_DIR = 'downloads-during-playwright'
export const getPlaywrightDownloadDir = (page: Page) => {
return path.resolve(page.dir, PLAYWRIGHT_DOWNLOAD_DIR)
}
const moveDownloadedFileTo = async (page: Page, toLocation: string) => {
await fsp.mkdir(path.dirname(toLocation), { recursive: true })
const downloadDir = getPlaywrightDownloadDir(page)
// Expect there to be at least one file
await expect
.poll(async () => {
const files = await fsp.readdir(downloadDir)
return files.length
})
.toBeGreaterThan(0)
// Go through the downloads dir and move files to new location
const files = await fsp.readdir(downloadDir)
// Assumption: only ever one file here.
for (let file of files) {
await fsp.rename(path.resolve(downloadDir, file), toLocation)
}
}
export interface Paths {
modelPath: string
imagePath: string
@ -870,8 +686,7 @@ export const doExport = async (
exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown'
): Promise<Paths> => {
if (exportFrom === 'dropdown') {
await page.getByTestId('project-sidebar-toggle').click()
await page.getByRole('button', { name: APP_NAME }).click()
const exportMenuButton = page.getByRole('button', {
name: 'Export current part',
})
@ -912,12 +727,25 @@ export const doExport = async (
}
await expect(page.getByText('Confirm Export')).toBeVisible()
const getPromiseAndResolve = () => {
let resolve: any = () => {}
const promise = new Promise<Download>((r) => {
resolve = r
})
return [promise, resolve]
}
const [downloadPromise1, downloadResolve1] = getPromiseAndResolve()
let downloadCnt = 0
if (exportFrom === 'dropdown')
page.on('download', async (download) => {
if (downloadCnt === 0) {
downloadResolve1(download)
}
downloadCnt++
})
await page.getByRole('button', { name: 'Submit command' }).click()
// This usually happens immediately after. If we're too slow we don't
// catch it.
await expect(page.getByText('Exported successfully')).toBeVisible()
if (exportFrom === 'sidebarButton' || exportFrom === 'commandBar') {
return {
modelPath: '',
@ -927,12 +755,15 @@ export const doExport = async (
}
// Handle download
const download = await downloadPromise1
const downloadLocationer = (extra = '', isImage = false) =>
`./e2e/playwright/export-snapshots/${output.type}-${
'storage' in output ? output.storage : ''
}${extra}.${isImage ? 'png' : output.type}`
const downloadLocation = downloadLocationer()
await download.saveAs(downloadLocation)
if (output.type === 'step') {
// stable timestamps for step files
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
@ -941,12 +772,6 @@ export const doExport = async (
'1970-01-01T00:00:00.0+00:00'
)
await fsp.writeFile(downloadLocation, newFileContents)
} else {
// By default all files are downloaded to the same place in playwright
// (declared in src/lib/exportSave)
// To remain consistent with our old web tests, we want to move some downloads
// (images) to another directory.
await moveDownloadedFileTo(page, downloadLocation)
}
return {
@ -973,8 +798,6 @@ export async function tearDown(page: Page, testInfo: TestInfo) {
// 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)
await testInfo.tronApp?.close()
}
// settingsOverrides may need to be augmented to take more generic items,
@ -985,24 +808,12 @@ export async function setup(
testInfo?: TestInfo
) {
await context.addInitScript(
async ({
token,
settingsKey,
settings,
IS_PLAYWRIGHT_KEY,
PLAYWRIGHT_TEST_DIR,
PERSIST_MODELING_CONTEXT,
}) => {
async ({ token, settingsKey, settings, IS_PLAYWRIGHT_KEY }) => {
localStorage.clear()
localStorage.setItem('TOKEN_PERSIST_KEY', token)
localStorage.setItem('persistCode', ``)
localStorage.setItem(
PERSIST_MODELING_CONTEXT,
JSON.stringify({ openPanes: ['code'] })
)
localStorage.setItem(settingsKey, settings)
localStorage.setItem(IS_PLAYWRIGHT_KEY, 'true')
localStorage.setItem('PLAYWRIGHT_TEST_DIR', PLAYWRIGHT_TEST_DIR)
},
{
token: secrets.token,
@ -1019,8 +830,6 @@ export async function setup(
} as Partial<SaveSettingsPayload>,
}),
IS_PLAYWRIGHT_KEY,
PLAYWRIGHT_TEST_DIR: TEST_SETTINGS.app.projectDirectory,
PERSIST_MODELING_CONTEXT,
}
)
@ -1039,15 +848,12 @@ export async function setup(
await page.emulateMedia({ reducedMotion: 'reduce' })
// Trigger a navigation, since loading file:// doesn't.
// await page.reload()
await page.reload()
}
let electronApp = undefined
let context = undefined
let page = undefined
export async function setupElectron({
testInfo,
folderSetupFn,
cleanProjectDir = true,
appSettings,
}: {
@ -1070,7 +876,7 @@ export async function setupElectron({
await fsp.mkdir(projectDirName)
}
const options = {
const electronApp = await electron.launch({
args: ['.', '--no-sandbox'],
env: {
...process.env,
@ -1080,22 +886,14 @@ export async function setupElectron({
...(process.env.ELECTRON_OVERRIDE_DIST_PATH
? { executablePath: process.env.ELECTRON_OVERRIDE_DIST_PATH + 'electron' }
: {}),
}
// Do this once and then reuse window on subsequent calls.
if (!electronApp) {
electronApp = await electron.launch(options)
}
if (!context || !page) {
context = electronApp.context()
page = await electronApp.firstWindow()
context.on('console', console.log)
page.on('console', console.log)
}
})
const context = electronApp.context()
const page = await electronApp.firstWindow()
context.on('console', console.log)
page.on('console', console.log)
if (cleanProjectDir) {
const tempSettingsFilePath = path.join(projectDirName, SETTINGS_FILE_NAME)
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify(
appSettings
? {
@ -1122,7 +920,11 @@ export async function setupElectron({
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
}
return { electronApp, page, context, dir: projectDirName, options }
await folderSetupFn?.(projectDirName)
await setup(context, page)
return { electronApp, page, dir: projectDirName }
}
function failOnConsoleErrors(page: Page, testInfo?: TestInfo) {
@ -1208,7 +1010,7 @@ export async function createProject({
}
export function executorInputPath(fileName: string): string {
return path.join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName)
return join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName)
}
export async function doAndWaitForImageDiff(
@ -1299,12 +1101,3 @@ export function getPixelRGBs(page: Page) {
})
}
}
export async function pollEditorLinesSelectedLength(page: Page, lines: number) {
return expect
.poll(async () => {
const lines = await page.locator('.cm-activeLine').all()
return lines.length
})
.toBe(lines)
}

View File

@ -1,14 +1,23 @@
import { test, expect } from './zoo-test'
import { test, expect } from '@playwright/test'
import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils'
import { getUtils } from './test-utils'
import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Testing Camera Movement', () => {
test('Can move camera reliably', async ({ page, context, homePage }) => {
test('Can move camera reliably', async ({ page, context }) => {
test.skip(process.platform === 'darwin', 'Can move camera reliably')
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel()
await u.closeKclCodePanel()
@ -174,7 +183,6 @@ test.describe('Testing Camera Movement', () => {
test('Zoom should be consistent when exiting or entering sketches', async ({
page,
homePage,
}) => {
// 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
@ -182,9 +190,9 @@ test.describe('Testing Camera Movement', () => {
test.skip(process.platform !== 'darwin', 'Zoom should be consistent')
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(
@ -336,10 +344,7 @@ test.describe('Testing Camera Movement', () => {
})
})
test(`Zoom by scroll should not fire while orbiting`, async ({
page,
homePage,
}) => {
test(`Zoom by scroll should not fire while orbiting`, async ({ page }) => {
/**
* Currently we only allow zooming by scroll when no other camera movement is happening,
* set within cameraMouseDragGuards in cameraControls.ts,
@ -378,7 +383,7 @@ test.describe('Testing Camera Movement', () => {
const expectedOrbitCamZPosition = 64.0
await test.step(`Test setup`, async () => {
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await u.closeKclCodePanel()
// This test requires the mouse controls to be set to Solidworks
await u.openDebugPanel()

View File

@ -1,32 +1,35 @@
import { test, expect } from './zoo-test'
import { test, expect } from '@playwright/test'
import {
getUtils,
TEST_COLORS,
pollEditorLinesSelectedLength,
} from './test-utils'
import { getUtils, setup, tearDown, TEST_COLORS } from './test-utils'
import { XOR } from 'lib/utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Testing constraints', () => {
test('Can constrain line length', async ({ page, homePage }) => {
test('Can constrain line length', async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> xLine(-20, %)
`
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> xLine(-20, %)
`
)
})
const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
@ -38,9 +41,7 @@ test.describe('Testing constraints', () => {
// enter sketch again
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
await page.waitForTimeout(500) // wait for animation
const startXPx = 500
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
@ -62,48 +63,42 @@ test.describe('Testing constraints', () => {
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await page.waitForTimeout(2500) // wait for animation
await page.waitForTimeout(500) // wait for animation
// Exit sketch
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await expect
.poll(async () => {
await page.keyboard.press('Escape', { delay: 500 })
return page.getByRole('button', { name: 'Exit Sketch' }).isVisible()
})
.toBe(true)
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
})
test(`Remove constraints`, async ({ page, homePage }) => {
test(`Remove constraints`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`yo = 79
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, 58.97 + 5], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 120], %)
|> xLine(-385.34, %, $seg_what)
|> yLine(-170.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
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, 58.97 + 5], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 120], %)
|> xLine(-385.34, %, $seg_what)
|> yLine(-170.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %, $seg01)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
const line3 = await u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`)
await page.mouse.click(line3.x, line3.y)
@ -116,8 +111,8 @@ test.describe('Testing constraints', () => {
await page.getByRole('button', { name: 'remove constraints' }).click()
await page.getByText('line([39.13, 68.63], %)').click()
await pollEditorLinesSelectedLength(page, 1)
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
@ -135,75 +130,43 @@ test.describe('Testing constraints', () => {
},
] as const
for (const { testName, offset } of cases) {
test(`${testName}`, async ({ page, homePage }) => {
test(`${testName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`yo = 5
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([51.19, 48.97], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
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([51.19, 48.97], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
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},`
)
await pollEditorLinesSelectedLength(page, 2)
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)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %, $seg01)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Give time for overlays to populate
await page.waitForTimeout(1000)
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.up('Shift')
await page.keyboard.down('Shift')
await page.waitForTimeout(100)
await page.mouse.click(line3.x, line3.y)
await page.waitForTimeout(100)
await page.waitForTimeout(100) // this wait is needed for webkit - not sure why
await page.keyboard.up('Shift')
await page.waitForTimeout(100)
await page
.getByRole('button', {
name: 'Length: open menu',
@ -231,7 +194,6 @@ test.describe('Testing constraints', () => {
`offset = ${offset},`
)
await pollEditorLinesSelectedLength(page, 2)
const activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(
`|> line([74.36, 130.4], %, $seg01)`
@ -267,37 +229,33 @@ test.describe('Testing constraints', () => {
},
] as const
for (const { testName, value, constraint } of cases) {
test(`${constraint} - ${testName}`, async ({ page, homePage }) => {
test(`${constraint} - ${testName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`yo = 5
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([51.19, 48.97], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([51.19, 48.97], %)
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.setBodyDimensions({ width: 1000, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
const [line1, line3] = await Promise.all([
u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`),
u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`),
@ -377,37 +335,33 @@ test.describe('Testing constraints', () => {
},
] as const
for (const { testName, addVariable, value, constraint } of cases) {
test(`${constraint} - ${testName}`, async ({ page, homePage }) => {
test(`${constraint} - ${testName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`yo = 5
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([51.19, 48.97], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([51.19, 48.97], %)
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.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
const [line3] = await Promise.all([
u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`),
])
@ -418,11 +372,9 @@ test.describe('Testing constraints', () => {
await page.mouse.click(900, 250)
}
await page.keyboard.down('Shift')
await page.waitForTimeout(100)
await page.mouse.click(line3.x, line3.y)
await page.waitForTimeout(100)
await page.waitForTimeout(100) // this wait is needed for webkit - not sure why
await page.keyboard.up('Shift')
await page.waitForTimeout(100)
await page
.getByRole('button', {
name: 'Length: open menu',
@ -490,37 +442,33 @@ test.describe('Testing constraints', () => {
},
] as const
for (const { testName, addVariable, value, axisSelect } of cases) {
test(`${testName}`, async ({ page, homePage }) => {
test(`${testName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`yo = 5
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([51.19, 48.97], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([51.19, 48.97], %)
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.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
const [line1, line3] = await Promise.all([
u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`),
u.getSegmentBodyCoords(`[data-overlay-index="${2}"]`),
@ -576,7 +524,7 @@ test.describe('Testing constraints', () => {
})
}
})
test.describe('Test Angle constraint single selection', () => {
test.describe('Test Angle/Length constraint single selection', () => {
const cases = [
{
testName: 'Angle - Add variable',
@ -590,70 +538,6 @@ test.describe('Testing constraints', () => {
constraint: 'angle',
value: '83, 78.33',
},
] as const
for (const { testName, addVariable, value, constraint } of cases) {
test(`${testName}`, async ({ page, homePage }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`yo = 5
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([51.19, 48.97], %)
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.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
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('Test Length constraint single selection', () => {
const cases = [
{
testName: 'Length - Add variable',
addVariable: true,
@ -668,17 +552,7 @@ test.describe('Testing constraints', () => {
},
] as const
for (const { testName, addVariable, value, constraint } of cases) {
test(`${testName}`, async ({ context, homePage, page }) => {
// constants and locators
const cmdBarKclInput = page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
const cmdBarKclVariableNameInput =
page.getByPlaceholder('Variable name')
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
test(`${testName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -698,9 +572,9 @@ part002 = startSketchOn('XZ')
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
@ -718,13 +592,11 @@ part002 = startSketchOn('XZ')
await page.getByTestId('dropdown-constraint-' + constraint).click()
if (!addVariable) {
await test.step(`Clear the variable input`, async () => {
await cmdBarKclVariableNameInput.clear()
await cmdBarKclVariableNameInput.press('Backspace')
})
await page.getByTestId('create-new-variable-checkbox').click()
}
await expect(cmdBarKclInput).toHaveText('78.33')
await cmdBarSubmitButton.click()
await page
.getByRole('button', { name: 'Add constraining value' })
.click()
const changedCode = `|> angledLine([${value}], %)`
await expect(page.locator('.cm-content')).toContainText(changedCode)
@ -756,37 +628,33 @@ part002 = startSketchOn('XZ')
},
] as const
for (const { codeAfter, constraintName } of cases) {
test(`${constraintName}`, async ({ page, homePage }) => {
test(`${constraintName}`, async ({ page }) => {
await page.addInitScript(async (customCode) => {
localStorage.setItem(
'persistCode',
`yo = 5
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([51.19, 48.97], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([51.19, 48.97], %)
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.setBodyDimensions({ width: 1000, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
const line1 = await u.getSegmentBodyCoords(
`[data-overlay-index="${0}"]`
)
@ -805,8 +673,8 @@ part002 = startSketchOn('XZ')
await page.keyboard.up('Shift')
// check actives lines
await pollEditorLinesSelectedLength(page, codeAfter.length)
const activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent).toHaveLength(codeAfter.length)
const constraintMenuButton = page.getByRole('button', {
name: 'Length: open menu',
@ -857,36 +725,32 @@ part002 = startSketchOn('XZ')
},
] as const
for (const { codeAfter, constraintName } of cases) {
test(`${constraintName}`, async ({ page, homePage }) => {
test(`${constraintName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`yo = 5
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
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.setBodyDimensions({ width: 1000, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
const line1 = await u.getBoundingBox(`[data-overlay-index="${0}"]`)
const line3 = await u.getBoundingBox(`[data-overlay-index="${2}"]`)
@ -913,8 +777,8 @@ part002 = startSketchOn('XZ')
// 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
await pollEditorLinesSelectedLength(page, 2)
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(
@ -938,47 +802,40 @@ part002 = startSketchOn('XZ')
},
] as const
for (const { codeAfter, constraintName, axisClick } of cases) {
test(`${constraintName}`, async ({ page, homePage }) => {
test(`${constraintName}`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`yo = 5
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
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.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
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.waitForTimeout(100)
await page.keyboard.down('Shift')
await page.waitForTimeout(100)
await page.mouse.click(axisClick.x, axisClick.y)
await page.waitForTimeout(100)
await page.keyboard.up('Shift')
await page.waitForTimeout(100)
const constraintMenuButton = page.getByRole('button', {
name: 'Length: open menu',
})
@ -1000,23 +857,21 @@ part002 = startSketchOn('XZ')
test('Horizontally constrained line remains selected after applying constraint', async ({
page,
homePage,
}) => {
test.setTimeout(70_000)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XY')
|> startProfileAt([-1.05, -1.07], %)
|> line([3.79, 2.68], %, $seg01)
|> line([3.13, -2.4], %)`
|> startProfileAt([-1.05, -1.07], %)
|> line([3.79, 2.68], %, $seg01)
|> line([3.13, -2.4], %)`
)
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.waitForAuthSkipAppStart()
await page.getByText('line([3.79, 2.68], %, $seg01)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeEnabled(
@ -1024,9 +879,6 @@ part002 = startSketchOn('XZ')
)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
// Wait for overlays to populate
await page.waitForTimeout(1000)
await page.waitForTimeout(100)
const lineBefore = await u.getSegmentBodyCoords(
`[data-overlay-index="1"]`,
@ -1047,17 +899,11 @@ part002 = startSketchOn('XZ')
name: 'Length: open menu',
})
.click()
await page.waitForTimeout(500)
await page.getByRole('button', { name: 'Horizontal', exact: true }).click()
await page.waitForTimeout(500)
await pollEditorLinesSelectedLength(page, 1)
let activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(`|> xLine(3.13, %)`)
// Wait for code editor to settle.
await page.waitForTimeout(2000)
// If the overlay-angle is updated the THREE.js scene is in a good state
await expect(
await page.locator('[data-overlay-index="1"]')
@ -1067,17 +913,11 @@ part002 = startSketchOn('XZ')
`[data-overlay-index="1"]`,
0
)
expect(
await u.getGreatestPixDiff(lineAfter, TEST_COLORS.BLUE)
).toBeLessThan(3)
const linebb = await u.getBoundingBox('[data-overlay-index="1"]')
await page.mouse.move(linebb.x, linebb.y, { steps: 25 })
await page.mouse.click(linebb.x, linebb.y)
await expect
.poll(async () => await u.getGreatestPixDiff(lineAfter, TEST_COLORS.BLUE))
.toBeLessThan(3)
await page.waitForTimeout(500)
await page.waitForTimeout(300)
await page
.getByRole('button', {
name: 'Length: open menu',
@ -1091,7 +931,6 @@ part002 = startSketchOn('XZ')
await page.getByLabel('length Value').fill('10')
await page.getByRole('button', { name: 'Add constraining value' }).click()
await pollEditorLinesSelectedLength(page, 1)
activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)

View File

@ -1,9 +1,18 @@
import { test, expect } from './zoo-test'
import { getUtils } from './test-utils'
import { _test, _expect } from './playwright-deprecated'
import { test } from './fixtures/fixtureSetup'
import { getUtils, setup, tearDown } from './test-utils'
import { uuidv4 } from 'lib/utils'
import { TEST_CODE_GIZMO } from './storageStates'
test.describe('Testing Gizmo', () => {
_test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
_test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
_test.describe('Testing Gizmo', () => {
const cases = [
{
testDescription: 'top view',
@ -48,17 +57,14 @@ test.describe('Testing Gizmo', () => {
expectedCameraTarget,
testDescription,
} of cases) {
test(`check ${testDescription}`, async ({ page, homePage }) => {
_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 page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.waitForAuthSkipAppStart()
await page.waitForTimeout(100)
// wait for execution done
await u.openDebugPanel()
@ -111,30 +117,30 @@ test.describe('Testing Gizmo', () => {
await Promise.all([
// position
expect(page.getByTestId('cam-x-position')).toHaveValue(
_expect(page.getByTestId('cam-x-position')).toHaveValue(
expectedCameraPosition.x.toString()
),
expect(page.getByTestId('cam-y-position')).toHaveValue(
_expect(page.getByTestId('cam-y-position')).toHaveValue(
expectedCameraPosition.y.toString()
),
expect(page.getByTestId('cam-z-position')).toHaveValue(
_expect(page.getByTestId('cam-z-position')).toHaveValue(
expectedCameraPosition.z.toString()
),
// target
expect(page.getByTestId('cam-x-target')).toHaveValue(
_expect(page.getByTestId('cam-x-target')).toHaveValue(
expectedCameraTarget.x.toString()
),
expect(page.getByTestId('cam-y-target')).toHaveValue(
_expect(page.getByTestId('cam-y-target')).toHaveValue(
expectedCameraTarget.y.toString()
),
expect(page.getByTestId('cam-z-target')).toHaveValue(
_expect(page.getByTestId('cam-z-target')).toHaveValue(
expectedCameraTarget.z.toString()
),
])
})
}
test('Context menu and popover menu', async ({ page, homePage }) => {
_test('Context menu and popover menu', async ({ page }) => {
const testCase = {
testDescription: 'Right view',
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
@ -146,9 +152,9 @@ test.describe('Testing Gizmo', () => {
await page.addInitScript((TEST_CODE_GIZMO) => {
localStorage.setItem('persistCode', TEST_CODE_GIZMO)
}, TEST_CODE_GIZMO)
await page.setBodyDimensions({ width: 1000, height: 500 })
await page.setViewportSize({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await page.waitForTimeout(100)
// wait for execution done
await u.openDebugPanel()
@ -190,7 +196,7 @@ test.describe('Testing Gizmo', () => {
const buttonToTest = page.getByRole('button', {
name: testCase.testDescription,
})
await expect(buttonToTest).toBeVisible()
await _expect(buttonToTest).toBeVisible()
await buttonToTest.click()
// Now assert we've moved to the correct view
@ -209,23 +215,23 @@ test.describe('Testing Gizmo', () => {
await Promise.all([
// position
expect(page.getByTestId('cam-x-position')).toHaveValue(
_expect(page.getByTestId('cam-x-position')).toHaveValue(
testCase.expectedCameraPosition.x.toString()
),
expect(page.getByTestId('cam-y-position')).toHaveValue(
_expect(page.getByTestId('cam-y-position')).toHaveValue(
testCase.expectedCameraPosition.y.toString()
),
expect(page.getByTestId('cam-z-position')).toHaveValue(
_expect(page.getByTestId('cam-z-position')).toHaveValue(
testCase.expectedCameraPosition.z.toString()
),
// target
expect(page.getByTestId('cam-x-target')).toHaveValue(
_expect(page.getByTestId('cam-x-target')).toHaveValue(
testCase.expectedCameraTarget.x.toString()
),
expect(page.getByTestId('cam-y-target')).toHaveValue(
_expect(page.getByTestId('cam-y-target')).toHaveValue(
testCase.expectedCameraTarget.y.toString()
),
expect(page.getByTestId('cam-z-target')).toHaveValue(
_expect(page.getByTestId('cam-z-target')).toHaveValue(
testCase.expectedCameraTarget.z.toString()
),
])
@ -236,59 +242,32 @@ test.describe('Testing Gizmo', () => {
const gizmoPopoverButton = page.getByRole('button', {
name: 'view settings',
})
await expect(gizmoPopoverButton).toBeVisible()
await _expect(gizmoPopoverButton).toBeVisible()
await gizmoPopoverButton.click()
await expect(buttonToTest).toBeVisible()
await _expect(buttonToTest).toBeVisible()
})
})
test.describe(`Testing gizmo, fixture-based`, () => {
test('Center on selection from menu', async ({
context,
page,
homePage,
app,
cmdBar,
editor,
toolbar,
scene,
}) => {
await context.addInitScript(() => {
localStorage.setItem(
'persistCode',
`
const sketch002 = startSketchOn('XZ')
|> startProfileAt([-108.83, -57.48], %)
|> angledLine([0, 105.13], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
77.9
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> close(%)
const sketch001 = startSketchOn('XZ')
|> circle({
center: [818.33, 168.1],
radius: 182.8
}, %)
|> extrude(50, %)
`
)
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
const u = await getUtils(page)
await u.waitForPageLoad()
test.skip(
process.platform === 'win32',
'Fails on windows in CI, can not be replicated locally on windows.'
)
await test.step(`Setup`, async () => {
const file = await app.getInputFile('test-circle-extrude.kcl')
await app.initialise(file)
await scene.expectState({
camera: {
position: [11912.6, -39586.98, 21391.21],
target: [11912.6, -635, 3317.49],
position: [4982.21, -23865.37, 13810.64],
target: [4982.21, 0, 2737.1],
},
})
})
@ -296,7 +275,7 @@ test.describe(`Testing gizmo, fixture-based`, () => {
await test.step(`Select an edge of this circle`, async () => {
const circleSnippet =
'circle({ center: [818.33, 168.1], radius: 182.8 }, %)'
'circle({ center = [318.33, 168.1], radius = 182.8 }, %)'
await moveToCircle()
await clickCircle()
await editor.expectState({
@ -313,8 +292,8 @@ test.describe(`Testing gizmo, fixture-based`, () => {
await test.step(`Verify the camera moved`, async () => {
await scene.expectState({
camera: {
position: [20785.58, -40221.98, 22343.46],
target: [20785.58, -1270, 4269.74],
position: [0, -23865.37, 11073.53],
target: [0, 0, 0],
},
})
})

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