Compare commits

..

3 Commits

Author SHA1 Message Date
87dfda28a9 Move axes to std constants; move helix, revolve, and mirror2d to be declated in KCL
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-04-02 17:05:28 +13:00
42f44e11f5 Add Edge type to std
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-03-28 11:18:10 +08:00
16ad7ff77a Move turns to a submodule of std
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-03-28 11:10:51 +08:00
1194 changed files with 103500 additions and 123089 deletions

View File

@ -1,3 +1,3 @@
[codespell] [codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser,fromM,FromM ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,./public/kcl-samples,./rust/kcl-lib/tests/kcl_samples,tsconfig.tsbuildinfo skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,./public/kcl-samples,./rust/kcl-lib/tests/kcl_samples,tsconfig.tsbuildinfo

79
.eslintrc Normal file
View File

@ -0,0 +1,79 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": [
"react-perf",
"css-modules",
"jest",
"jsx-a11y",
"react",
"react-hooks",
"suggest-no-throw",
"testing-library",
"@typescript-eslint"
],
"extends": [
"plugin:css-modules/recommended",
"plugin:jsx-a11y/recommended",
"plugin:react-hooks/recommended"
],
"rules": {
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-unused-vars": ["error", {
"varsIgnorePattern": "^_",
"argsIgnorePattern": "^_",
"ignoreRestSiblings": true,
"vars": "all",
"args": "none"
}],
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-autofocus": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"no-restricted-globals": [
"error",
{
"name": "isNaN",
"message": "Use Number.isNaN() instead."
},
],
"no-restricted-syntax": [
"error",
{
"selector": "CallExpression[callee.object.name='Array'][callee.property.name='isArray']",
"message": "Use isArray() in lib/utils.ts instead of Array.isArray()."
}
],
"semi": [
"error",
"never"
],
"react-hooks/exhaustive-deps": "off",
"suggest-no-throw/suggest-no-throw": "warn",
},
"overrides": [
{
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
"extends": [
"plugin:testing-library/react"
],
"rules": {
"suggest-no-throw/suggest-no-throw": "off",
"testing-library/prefer-screen-queries": "off",
"jest/valid-expect": "off"
}
},
{
"files": ["src/**/*.test.ts"],
"extends": [
"plugin:testing-library/react"
],
"rules": {
"suggest-no-throw/suggest-no-throw": "off",
}
}
]
}

View File

@ -1,127 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": [
"react-perf",
"css-modules",
"jest",
"jsx-a11y",
"react",
"react-hooks",
"suggest-no-throw",
"testing-library",
"@typescript-eslint"
],
"extends": [
"plugin:css-modules/recommended",
"plugin:jsx-a11y/recommended",
"plugin:react-hooks/recommended"
],
"rules": {
"no-array-constructor": "off", // This is wrong; use the @typescript-eslint one instead.
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-array-delete": "error",
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-duplicate-type-constituents": "error",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error",
"no-implied-eval": "off", // This is wrong; use the @typescript-eslint one instead.
"@typescript-eslint/no-implied-eval": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-redundant-type-constituents": "error",
"@typescript-eslint/no-this-alias": "warn",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"no-unused-vars": "off", // This is wrong; use the @typescript-eslint one instead.
"@typescript-eslint/no-unused-vars": [
"error",
{
"varsIgnorePattern": "^_",
"argsIgnorePattern": "^_",
"ignoreRestSiblings": true,
"vars": "all",
"args": "none"
}
],
"@typescript-eslint/no-unsafe-unary-minus": "error",
"@typescript-eslint/no-wrapper-object-types": "error",
"no-throw-literal": "off", // Use @typescript-eslint/only-throw-error instead.
"@typescript-eslint/only-throw-error": "error",
"@typescript-eslint/prefer-as-const": "warn",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-autofocus": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"no-restricted-globals": [
"error",
{
"name": "isNaN",
"message": "Use Number.isNaN() instead."
}
],
"no-restricted-syntax": [
"error",
{
"selector": "CallExpression[callee.object.name='Array'][callee.property.name='isArray']",
"message": "Use isArray() in lib/utils.ts instead of Array.isArray()."
},
{
"selector": "CallExpression[callee.object.name='TOML'][callee.property.name='stringify']",
"message": "Do not use TOML.stringify directly. Use the wrappers in test-utils instead like settingsToToml."
},
{
"selector": "CallExpression[callee.object.name='TOML'][callee.property.name='parse']",
"message": "Do not use TOML.parse directly. Use the wrappers in test-utils instead like tomlToSettings."
}
],
"no-restricted-imports": [
"error",
{
"patterns": [
// Restrict all relative imports except for .css files.
{
"group": ["./*", "../*", "!./*.css", "!../*.css"],
"message": "Use absolute imports instead."
}
]
}
],
"semi": ["error", "never"],
"react-hooks/exhaustive-deps": "off",
"suggest-no-throw/suggest-no-throw": "error"
},
"overrides": [
{
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
"extends": ["plugin:testing-library/react"],
"rules": {
"suggest-no-throw/suggest-no-throw": "off",
"testing-library/prefer-screen-queries": "off",
"jest/valid-expect": "off"
}
},
{
"files": ["src/**/*.test.ts"],
"extends": ["plugin:testing-library/react"],
"rules": {
"suggest-no-throw/suggest-no-throw": "off"
}
},
{
"files": ["packages/**/*.ts", "rust/**/*.ts"],
"extends": [],
"rules": {
"no-restricted-imports": "off"
}
}
]
}

View File

@ -1,5 +1,5 @@
name: Bug Report name: Bug Report
description: File a bug report for the Zoo Design Studio description: File a bug report for the Zoo Modeling App
title: "[BUG]: " title: "[BUG]: "
labels: ["bug"] labels: ["bug"]
assignees: [] assignees: []
@ -70,7 +70,7 @@ body:
id: version id: version
attributes: attributes:
label: Version label: Version
description: "The version of the Zoo Design Studio you're using." description: "The version of the Zoo Modeling App you're using."
placeholder: "example: v0.15.0. You can find this in the settings." placeholder: "example: v0.15.0. You can find this in the settings."
validations: validations:
required: true required: true

View File

@ -1,24 +0,0 @@
#!/bin/bash
set -euo pipefail
# Install vector
brew tap vectordotdev/brew && brew install vector
# Configure vector
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i '' "s#OS_NAME#${OS_NAME}#g" /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#${GH_ACTIONS_AXIOM_TOKEN}#g" /tmp/vector.toml
# Display settings
echo
echo 'Vector config:'
cat /tmp/vector.toml
echo
# Start in the background
$(brew --prefix)/opt/vector/bin/vector --config /tmp/vector.toml &

View File

@ -1,24 +0,0 @@
#!/bin/bash
set -euo pipefail
# Install vector
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev | bash -s -- -y
# Configure vector
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "s#OS_NAME#${OS_NAME}#g" /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#${GH_ACTIONS_AXIOM_TOKEN}#g" /tmp/vector.toml
# Display settings
echo
echo 'Vector config:'
cat /tmp/vector.toml
echo
# Start in background
${HOME}/.vector/bin/vector --config /tmp/vector.toml &

View File

@ -24,7 +24,7 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
cache: false # Configured below. cache: false # Configured below.
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc - uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
with: with:
tool: wasm-pack tool: wasm-pack
- name: Rust Cache - name: Rust Cache

View File

@ -77,7 +77,7 @@ jobs:
with: with:
cache: false # Configured below. cache: false # Configured below.
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc - uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }} if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
with: with:
tool: wasm-pack tool: wasm-pack
@ -241,7 +241,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: out-arm64-${{ matrix.platform }} name: out-arm64-${{ matrix.platform }}
# first two will pick both Zoo Design Studio-$VERSION-arm64-win.exe and Zoo Design Studio-$VERSION-win.exe # first two will pick both Zoo Modeling App-$VERSION-arm64-win.exe and Zoo Modeling App-$VERSION-win.exe
path: | path: |
out/*-${{ env.VERSION_NO_V }}-win.* out/*-${{ env.VERSION_NO_V }}-win.*
out/*-${{ env.VERSION_NO_V }}-arm64-win.* out/*-${{ env.VERSION_NO_V }}-arm64-win.*

View File

@ -34,11 +34,20 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
cache: false # Configured below. cache: false # Configured below.
- name: Start Vector - name: Install vector
run: .github/ci-cd-scripts/start-vector-ubuntu.sh run: |
env: curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
GH_ACTIONS_AXIOM_TOKEN: ${{ secrets.GH_ACTIONS_AXIOM_TOKEN }} chmod +x /tmp/vector.sh
OS_NAME: ${{ env.OS_NAME }} /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 &
- uses: taiki-e/install-action@cargo-llvm-cov - uses: taiki-e/install-action@cargo-llvm-cov
- uses: taiki-e/install-action@nextest - uses: taiki-e/install-action@nextest
- name: Install just - name: Install just

View File

@ -140,7 +140,7 @@ jobs:
with: with:
cache: false # Configured below. cache: false # Configured below.
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc - uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
if: ${{ needs.conditions.outputs.should-run == 'true' && steps.wasm.outputs.should-build-wasm == 'true' }} if: ${{ needs.conditions.outputs.should-run == 'true' && steps.wasm.outputs.should-build-wasm == 'true' }}
with: with:
tool: wasm-pack tool: wasm-pack
@ -229,6 +229,10 @@ jobs:
timeout_minutes: 30 timeout_minutes: 30
max_attempts: 3 max_attempts: 3
env: env:
CI: true
NODE_ENV: development
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }} snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
@ -281,7 +285,7 @@ jobs:
os: os:
- "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64" - "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
- namespace-profile-macos-8-cores - namespace-profile-macos-8-cores
- windows-latest-8-cores - windows-latest
shardIndex: [1, 2, 3, 4] shardIndex: [1, 2, 3, 4]
shardTotal: [4] shardTotal: [4]
# Disable macos and windows tests on hourly e2e tests since we only care # Disable macos and windows tests on hourly e2e tests since we only care
@ -292,7 +296,7 @@ jobs:
exclude: exclude:
- os: namespace-profile-macos-8-cores - os: namespace-profile-macos-8-cores
isScheduled: true isScheduled: true
- os: windows-latest-8-cores - os: windows-latest
isScheduled: true isScheduled: true
# TODO: add ref here for main and latest release tag # TODO: add ref here for main and latest release tag
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -339,12 +343,22 @@ jobs:
if: needs.conditions.outputs.should-run == 'true' if: needs.conditions.outputs.should-run == 'true'
run: yarn tronb:vite:dev run: yarn tronb:vite:dev
- name: Start Vector - name: Install vector
if: ${{ needs.conditions.outputs.should-run == 'true' && !contains(matrix.os, 'windows') }} if: ${{ needs.conditions.outputs.should-run == 'true' && contains(matrix.os, 'ubuntu') }}
run: .github/ci-cd-scripts/start-vector-${{ env.OS_NAME }}.sh shell: bash
env: run: |
GH_ACTIONS_AXIOM_TOKEN: ${{ secrets.GH_ACTIONS_AXIOM_TOKEN }} curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
OS_NAME: ${{ env.OS_NAME }} 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 &
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && !cancelled() && (success() || failure()) }} if: ${{ needs.conditions.outputs.should-run == 'true' && !cancelled() && (success() || failure()) }}
@ -360,10 +374,14 @@ jobs:
with: with:
shell: bash shell: bash
command: .github/ci-cd-scripts/playwright-electron.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{ env.OS_NAME }} command: .github/ci-cd-scripts/playwright-electron.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{ env.OS_NAME }}
timeout_minutes: 30 timeout_minutes: 45
max_attempts: 9 max_attempts: 15
env: env:
CI: true
FAIL_ON_CONSOLE_ERRORS: true FAIL_ON_CONSOLE_ERRORS: true
NODE_ENV: development
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4

View File

@ -28,87 +28,43 @@ jobs:
- run: yarn fmt-check - run: yarn fmt-check
yarn-build-wasm: yarn-build-wasm:
# Build the wasm blob once on the fastest runner. runs-on: ubuntu-22.04
runs-on: runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- run: yarn install
- name: Install dependencies - uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
run: yarn install
- name: Use correct Rust toolchain
shell: bash
run: |
[ -e rust-toolchain.toml ] || cp rust/rust-toolchain.toml ./
- name: Install rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false # Configured below.
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc
with: with:
tool: wasm-pack tool: wasm-pack
- run: yarn build:wasm
- name: Rust Cache yarn-tsc:
uses: Swatinem/rust-cache@v2 runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn --cwd ./rust/kcl-language-server --modules-folder node_modules install
- uses: Swatinem/rust-cache@v2
with: with:
workspaces: './rust' workspaces: './rust'
- name: Build Wasm - uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
shell: bash
run: yarn build:wasm
- uses: actions/upload-artifact@v4
with: with:
name: prepared-wasm tool: wasm-pack
path: | - run: yarn build:wasm
rust/kcl-wasm-lib/pkg/kcl_wasm_lib*
- uses: actions/upload-artifact@v4
with:
name: prepared-ts-rs-bindings
path: |
rust/kcl-lib/bindings/*
yarn-tsc:
runs-on: ubuntu-latest
needs: yarn-build-wasm
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Copy prepared wasm
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
mkdir rust/kcl-wasm-lib/pkg
cp prepared-wasm/kcl_wasm_lib* rust/kcl-wasm-lib/pkg
- name: Copy prepared ts-rs bindings
run: |
ls -R prepared-ts-rs-bindings
mkdir rust/kcl-lib/bindings
cp -r prepared-ts-rs-bindings/* rust/kcl-lib/bindings/
- run: yarn tsc - run: yarn tsc
yarn-lint: yarn-lint:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
needs: yarn-build-wasm
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -117,55 +73,9 @@ jobs:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- run: yarn install - run: yarn install
- run: yarn --cwd ./rust/kcl-language-server --modules-folder node_modules install
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Copy prepared wasm
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
mkdir rust/kcl-wasm-lib/pkg
cp prepared-wasm/kcl_wasm_lib* rust/kcl-wasm-lib/pkg
- name: Copy prepared ts-rs bindings
run: |
ls -R prepared-ts-rs-bindings
mkdir rust/kcl-lib/bindings
cp -r prepared-ts-rs-bindings/* rust/kcl-lib/bindings/
- run: yarn lint - run: yarn lint
yarn-circular-dependencies:
runs-on: ubuntu-latest
needs: yarn-build-wasm
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Copy prepared wasm
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
mkdir rust/kcl-wasm-lib/pkg
cp prepared-wasm/kcl_wasm_lib* rust/kcl-wasm-lib/pkg
- name: Copy prepared ts-rs bindings
run: |
ls -R prepared-ts-rs-bindings
mkdir rust/kcl-lib/bindings
cp -r prepared-ts-rs-bindings/* rust/kcl-lib/bindings/
- run: yarn circular-deps:diff
python-codespell: python-codespell:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@ -181,7 +91,6 @@ jobs:
yarn-unit-test-kcl-samples: yarn-unit-test-kcl-samples:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: yarn-build-wasm
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -191,25 +100,10 @@ jobs:
cache: 'yarn' cache: 'yarn'
- run: yarn install - run: yarn install
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc - uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
with: with:
tool: wasm-pack tool: wasm-pack
- run: yarn build:wasm
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Copy prepared wasm
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
mkdir rust/kcl-wasm-lib/pkg
cp prepared-wasm/kcl_wasm_lib* rust/kcl-wasm-lib/pkg
- name: Copy prepared ts-rs bindings
run: |
ls -R prepared-ts-rs-bindings
mkdir rust/kcl-lib/bindings
cp -r prepared-ts-rs-bindings/* rust/kcl-lib/bindings/
- run: yarn simpleserver:bg - run: yarn simpleserver:bg
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }} if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
@ -226,7 +120,6 @@ jobs:
yarn-unit-test: yarn-unit-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: yarn-build-wasm
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -236,25 +129,10 @@ jobs:
cache: 'yarn' cache: 'yarn'
- run: yarn install - run: yarn install
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc - uses: taiki-e/install-action@37bdc826eaedac215f638a96472df572feab0f9b
with: with:
tool: wasm-pack tool: wasm-pack
- run: yarn build:wasm
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Copy prepared wasm
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
mkdir rust/kcl-wasm-lib/pkg
cp prepared-wasm/kcl_wasm_lib* rust/kcl-wasm-lib/pkg
- name: Copy prepared ts-rs bindings
run: |
ls -R prepared-ts-rs-bindings
mkdir rust/kcl-lib/bindings
cp -r prepared-ts-rs-bindings/* rust/kcl-lib/bindings/
- run: yarn simpleserver:bg - run: yarn simpleserver:bg
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }} if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}

View File

@ -8,7 +8,6 @@ include = ["/tmp/github-actions.log"]
type = "remap" type = "remap"
inputs = [ "github-actions-file" ] inputs = [ "github-actions-file" ]
source = ''' source = '''
.platform = "OS_NAME"
.action = "GITHUB_WORKFLOW" .action = "GITHUB_WORKFLOW"
.repo = "GITHUB_REPOSITORY" .repo = "GITHUB_REPOSITORY"
.sha = "GITHUB_SHA" .sha = "GITHUB_SHA"

View File

@ -1,27 +1,27 @@
# Setting Up Zoo Design Studio # Setting Up Zoo Modeling App
Compared to other CAD software, getting Zoo Design Studio up and running is quick and straightforward across platforms. It's about 100MB to download and is quick to install. 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 ## Windows
1. Download the [Zoo Design Studio installer](https://zoo.dev/modeling-app/download) for Windows and for your processor type. 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 Design Studio-{version}-{arch}-win.exe` which should take a few seconds. 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 Design Studio`. A shortcut in the start menu is also created so you can run the app easily by clicking on it. 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 ## macOS
1. Download the [Zoo Design Studio installer](https://zoo.dev/modeling-app/download) for macOS and for your processor type. 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 Design Studio-{version}-{arch}-mac.dmg` and drag the applications to your `Applications` directory. 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 Design Studio` to open. 3. You can then open your `Applications` directory and double-click on `Zoo Modeling App` to open.
## Linux ## Linux
1. Download the [Zoo Design Studio installer](https://zoo.dev/modeling-app/download) for Linux and for your processor type. 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/). 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. - On Ubuntu, install the FUSE library with these commands in a terminal.
@ -30,7 +30,7 @@ Compared to other CAD software, getting Zoo Design Studio up and running is quic
sudo apt install libfuse2 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. - 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 Design Studio-{version}-{arch}-linux.AppImage` to the directory of your choice, for instance `~/Applications`. - 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: - `appimaged` should automatically find it and make it executable. If not, run:
```bash ```bash

View File

@ -4,38 +4,18 @@ all: install build check
############################################################################### ###############################################################################
# INSTALL # INSTALL
ifeq ($(OS),Windows_NT) WASM_PACK ?= ~/.cargo/bin/wasm-pack
CARGO ?= ~/.cargo/bin/cargo.exe
WASM_PACK ?= ~/.cargo/bin/wasm-pack.exe
else
CARGO ?= ~/.cargo/bin/cargo
WASM_PACK ?= ~/.cargo/bin/wasm-pack
endif
.PHONY: install .PHONY: install
install: node_modules/.yarn-integrity $(CARGO) $(WASM_PACK) ## Install dependencies install: node_modules/.yarn-integrity $(WASM_PACK) ## Install dependencies
node_modules/.yarn-integrity: package.json yarn.lock node_modules/.yarn-integrity: package.json yarn.lock
yarn install yarn install
ifeq ($(OS),Windows_NT)
@ type nul > $@
else
@ touch $@ @ touch $@
endif
$(CARGO):
ifeq ($(OS),Windows_NT)
yarn install:rust:windows
else
yarn install:rust
endif
$(WASM_PACK): $(WASM_PACK):
ifeq ($(OS),Windows_NT) yarn install:rust
yarn install:wasm-pack:cargo
else
yarn install:wasm-pack:sh yarn install:wasm-pack:sh
endif
############################################################################### ###############################################################################
# BUILD # BUILD
@ -51,17 +31,13 @@ VITE_SOURCES := $(wildcard vite.*) $(wildcard vite/**/*.tsx)
build: build-web build-desktop build: build-web build-desktop
.PHONY: build-web .PHONY: build-web
build-web: install public/kcl_wasm_lib_bg.wasm build/index.html build-web: public/kcl_wasm_lib_bg.wasm build/index.html
.PHONY: build-desktop .PHONY: build-desktop
build-desktop: install public/kcl_wasm_lib_bg.wasm .vite/build/main.js build-desktop: public/kcl_wasm_lib_bg.wasm .vite/build/main.js
public/kcl_wasm_lib_bg.wasm: $(CARGO_SOURCES) $(RUST_SOURCES) public/kcl_wasm_lib_bg.wasm: $(CARGO_SOURCES)$(RUST_SOURCES)
ifeq ($(OS),Windows_NT) yarn build:wasm
yarn build:wasm:dev:windows
else
yarn build:wasm:dev
endif
build/index.html: $(REACT_SOURCES) $(TYPESCRIPT_SOURCES) $(VITE_SOURCES) build/index.html: $(REACT_SOURCES) $(TYPESCRIPT_SOURCES) $(VITE_SOURCES)
yarn build:local yarn build:local
@ -87,10 +63,8 @@ lint: install ## Lint the code
############################################################################### ###############################################################################
# RUN # RUN
TARGET ?= desktop
.PHONY: run .PHONY: run
run: run-$(TARGET) run: run-web
.PHONY: run-web .PHONY: run-web
run-web: install build-web ## Start the web app run-web: install build-web ## Start the web app
@ -103,9 +77,9 @@ run-desktop: install build-desktop ## Start the desktop app
############################################################################### ###############################################################################
# TEST # TEST
E2E_GREP ?= E2E_WORKERS ?= 1
E2E_WORKERS ?=
E2E_FAILURES ?= 1 E2E_FAILURES ?= 1
E2E_GREP ?= ""
.PHONY: test .PHONY: test
test: test-unit test-e2e test: test-unit test-e2e
@ -116,47 +90,31 @@ test-unit: install ## Run the unit tests
yarn test:unit yarn test:unit
.PHONY: test-e2e .PHONY: test-e2e
test-e2e: test-e2e-$(TARGET) test-e2e: test-e2e-desktop
.PHONY: test-e2e-web .PHONY: test-e2e-web
test-e2e-web: install build-web ## Run the web e2e tests test-e2e-web: install build-web ## Run the web e2e tests
@ curl -fs localhost:3000 >/dev/null || ( echo "Error: localhost:3000 not available, 'make run-web' first" && exit 1 ) @ curl -fs localhost:3000 >/dev/null || ( echo "Error: localhost:3000 not available, 'make run-web' first" && exit 1 )
ifdef E2E_GREP yarn chrome:test --headed --workers=$(E2E_WORKERS) --max-failures=$(E2E_FAILURES) --grep=$(E2E_GREP)
yarn chrome:test --headed --grep="$(E2E_GREP)" --max-failures=$(E2E_FAILURES)
else
yarn chrome:test --headed --workers='100%'
endif
.PHONY: test-e2e-desktop .PHONY: test-e2e-desktop
test-e2e-desktop: install build-desktop ## Run the desktop e2e tests test-e2e-desktop: install build-desktop ## Run the desktop e2e tests
ifdef E2E_GREP yarn test:playwright:electron --workers=$(E2E_WORKERS) --max-failures=$(E2E_FAILURES) --grep=$(E2E_GREP)
yarn test:playwright:electron --grep="$(E2E_GREP)" --max-failures=$(E2E_FAILURES)
else
yarn test:playwright:electron --workers='100%'
endif
############################################################################### ###############################################################################
# CLEAN # CLEAN
.PHONY: clean .PHONY: clean
clean: ## Delete all artifacts clean: ## Delete all artifacts
ifeq ($(OS),Windows_NT)
git clean --force -d -X
else
rm -rf .vite/ build/ rm -rf .vite/ build/
rm -rf trace.zip playwright-report/ test-results/ rm -rf trace.zip playwright-report/ test-results/
rm -rf public/kcl_wasm_lib_bg.wasm rm -rf public/kcl_wasm_lib_bg.wasm
rm -rf rust/*/bindings/ rust/*/pkg/ rust/target/ rm -rf rust/*/bindings/ rust/*/pkg/ rust/target/
rm -rf node_modules/ rust/*/node_modules/ rm -rf node_modules/ rust/*/node_modules/
endif
.PHONY: help .PHONY: help
help: install help: install
ifeq ($(OS),Windows_NT)
@ powershell -Command "Get-Content $(MAKEFILE_LIST) | Select-String -Pattern '^[^\s]+:.*##\s.*$$' | ForEach-Object { $$line = $$_.Line -split ':.*?##\s+'; Write-Host -NoNewline $$line[0].PadRight(30) -ForegroundColor Cyan; Write-Host $$line[1] }"
else
@ grep -E '^[^[:space:]]+:.*## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @ grep -E '^[^[:space:]]+:.*## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
endif
.DEFAULT_GOAL := help .DEFAULT_GOAL := help

View File

@ -1,17 +1,17 @@
![Zoo Design Studio](/public/zma-logomark-outlined.png) ![Zoo Modeling App](/public/zma-logomark-outlined.png)
## Zoo Design Studio ## Zoo Modeling App
download at [zoo.dev/modeling-app/download](https://zoo.dev/modeling-app/download) download at [zoo.dev/modeling-app/download](https://zoo.dev/modeling-app/download)
A CAD application from the future, brought to you by the [Zoo team](https://zoo.dev). A CAD application from the future, brought to you by the [Zoo team](https://zoo.dev).
Design Studio is our take on what a modern modelling experience can be. It is applying several lessons learned in the decades since most major CAD tools came into existence: Modeling App is our take on what a modern modelling experience can be. It is applying several lessons learned in the decades since most major CAD tools came into existence:
- All artifacts—including parts and assemblies—should be represented as human-readable code. At the end of the day, your CAD project should be "plain text" - All artifacts—including parts and assemblies—should be represented as human-readable code. At the end of the day, your CAD project should be "plain text"
- This makes version control—which is a solved problem in software engineering—trivial for CAD - This makes version control—which is a solved problem in software engineering—trivial for CAD
- All GUI (or point-and-click) interactions should be actions performed on this code representation under the hood - All GUI (or point-and-click) interactions should be actions performed on this code representation under the hood
- This unlocks a hybrid approach to modeling. Whether you point-and-click as you always have or you write your own KCL code, you are performing the same action in Design Studio - This unlocks a hybrid approach to modeling. Whether you point-and-click as you always have or you write your own KCL code, you are performing the same action in Modeling App
- Everything graphics _has_ to be built for the GPU - Everything graphics _has_ to be built for the GPU
- Most CAD applications have had to retrofit support for GPUs, but our geometry engine is made for GPUs (primarily Nvidia's Vulkan), getting the order of magnitude rendering performance boost with it - Most CAD applications have had to retrofit support for GPUs, but our geometry engine is made for GPUs (primarily Nvidia's Vulkan), getting the order of magnitude rendering performance boost with it
- Make the resource-intensive pieces of an application auto-scaling - Make the resource-intensive pieces of an application auto-scaling
@ -19,9 +19,9 @@ Design Studio is our take on what a modern modelling experience can be. It is ap
We are excited about what a small team of people could build in a short time with our API. We welcome you to try our API, build your own applications, or contribute to ours! We are excited about what a small team of people could build in a short time with our API. We welcome you to try our API, build your own applications, or contribute to ours!
Design Studio is a _hybrid_ user interface for CAD modeling. You can point-and-click to design parts (and soon assemblies), but everything you make is really just [`kcl` code](https://github.com/KittyCAD/kcl-experiments) under the hood. All of your CAD models can be checked into source control such as GitHub and responsibly versioned, rolled back, and more. Modeling App is a _hybrid_ user interface for CAD modeling. You can point-and-click to design parts (and soon assemblies), but everything you make is really just [`kcl` code](https://github.com/KittyCAD/kcl-experiments) under the hood. All of your CAD models can be checked into source control such as GitHub and responsibly versioned, rolled back, and more.
The 3D view in Design Studio is just a video stream from our hosted geometry engine. The app sends new modeling commands to the engine via WebSockets, which returns back video frames of the view within the engine. The 3D view in Modeling App is just a video stream from our hosted geometry engine. The app sends new modeling commands to the engine via WebSockets, which returns back video frames of the view within the engine.
## Tools ## Tools
@ -198,13 +198,13 @@ If the prompt doesn't show up, start the app in command line to grab the electro
``` ```
# Windows (PowerShell) # Windows (PowerShell)
& 'C:\Program Files\Zoo Design Studio\Zoo Design Studio.exe' & 'C:\Program Files\Zoo Modeling App\Zoo Modeling App.exe'
# macOS # macOS
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App /Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
# Linux # Linux
./Zoo Design Studio-{version}-{arch}-linux.AppImage ./Zoo Modeling App-{version}-{arch}-linux.AppImage
``` ```
#### 4. Publish the release #### 4. Publish the release

File diff suppressed because one or more lines are too long

View File

@ -18,7 +18,7 @@ std::math::PI: number = 3.14159265358979323846264338327950288_
circumference = 70 circumference = 70
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> circle(center = [0, 0], radius = circumference / (2 * PI)) |> circle(center = [0, 0], radius = circumference/ (2 * PI))
example = extrude(exampleSketch, length = 5) example = extrude(exampleSketch, length = 5)
``` ```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,7 @@ Import a CAD file.
For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory. For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.
Note: The import command currently only works when using the native Design Studio. Note: The import command currently only works when using the native Modeling App.
```js ```js
import( import(

40
docs/kcl/inch.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
--- ---
title: "KCL Standard Library" title: "KCL Standard Library"
excerpt: "Documentation for the KCL standard library for the Zoo Design Studio." excerpt: "Documentation for the KCL standard library for the Zoo Modeling App."
layout: manual layout: manual
--- ---
@ -65,15 +65,11 @@ layout: manual
* [`chamfer`](kcl/chamfer) * [`chamfer`](kcl/chamfer)
* [`circleThreePoint`](kcl/circleThreePoint) * [`circleThreePoint`](kcl/circleThreePoint)
* [`close`](kcl/close) * [`close`](kcl/close)
* [`cm`](kcl/cm)
* [`extrude`](kcl/extrude) * [`extrude`](kcl/extrude)
* [`fillet`](kcl/fillet) * [`fillet`](kcl/fillet)
* [`floor`](kcl/floor) * [`floor`](kcl/floor)
* [`fromCm`](kcl/fromCm) * [`ft`](kcl/ft)
* [`fromFt`](kcl/fromFt)
* [`fromInches`](kcl/fromInches)
* [`fromM`](kcl/fromM)
* [`fromMm`](kcl/fromMm)
* [`fromYd`](kcl/fromYd)
* [`getCommonEdge`](kcl/getCommonEdge) * [`getCommonEdge`](kcl/getCommonEdge)
* [`getNextAdjacentEdge`](kcl/getNextAdjacentEdge) * [`getNextAdjacentEdge`](kcl/getNextAdjacentEdge)
* [`getOppositeEdge`](kcl/getOppositeEdge) * [`getOppositeEdge`](kcl/getOppositeEdge)
@ -81,6 +77,7 @@ layout: manual
* [`helix`](kcl/std-helix) * [`helix`](kcl/std-helix)
* [`hole`](kcl/hole) * [`hole`](kcl/hole)
* [`hollow`](kcl/hollow) * [`hollow`](kcl/hollow)
* [`inch`](kcl/inch)
* [`lastSegX`](kcl/lastSegX) * [`lastSegX`](kcl/lastSegX)
* [`lastSegY`](kcl/lastSegY) * [`lastSegY`](kcl/lastSegY)
* [`legAngX`](kcl/legAngX) * [`legAngX`](kcl/legAngX)
@ -92,9 +89,11 @@ layout: manual
* [`log`](kcl/log) * [`log`](kcl/log)
* [`log10`](kcl/log10) * [`log10`](kcl/log10)
* [`log2`](kcl/log2) * [`log2`](kcl/log2)
* [`m`](kcl/m)
* [`map`](kcl/map) * [`map`](kcl/map)
* [`max`](kcl/max) * [`max`](kcl/max)
* [`min`](kcl/min) * [`min`](kcl/min)
* [`mm`](kcl/mm)
* [`offsetPlane`](kcl/offsetPlane) * [`offsetPlane`](kcl/offsetPlane)
* [`patternCircular2d`](kcl/patternCircular2d) * [`patternCircular2d`](kcl/patternCircular2d)
* [`patternCircular3d`](kcl/patternCircular3d) * [`patternCircular3d`](kcl/patternCircular3d)
@ -102,6 +101,7 @@ layout: manual
* [`patternLinear3d`](kcl/patternLinear3d) * [`patternLinear3d`](kcl/patternLinear3d)
* [`patternTransform`](kcl/patternTransform) * [`patternTransform`](kcl/patternTransform)
* [`patternTransform2d`](kcl/patternTransform2d) * [`patternTransform2d`](kcl/patternTransform2d)
* [`polar`](kcl/polar)
* [`polygon`](kcl/polygon) * [`polygon`](kcl/polygon)
* [`pop`](kcl/pop) * [`pop`](kcl/pop)
* [`pow`](kcl/pow) * [`pow`](kcl/pow)
@ -137,12 +137,12 @@ layout: manual
* [`translate`](kcl/translate) * [`translate`](kcl/translate)
* [`xLine`](kcl/xLine) * [`xLine`](kcl/xLine)
* [`yLine`](kcl/yLine) * [`yLine`](kcl/yLine)
* [`yd`](kcl/yd)
* **std::math** * **std::math**
* [`E`](kcl/consts/std-math-E) * [`E`](kcl/consts/std-math-E)
* [`PI`](kcl/consts/std-math-PI) * [`PI`](kcl/consts/std-math-PI)
* [`TAU`](kcl/consts/std-math-TAU) * [`TAU`](kcl/consts/std-math-TAU)
* [`cos`](kcl/std-math-cos) * [`cos`](kcl/std-math-cos)
* [`polar`](kcl/std-math-polar)
* [`sin`](kcl/std-math-sin) * [`sin`](kcl/std-math-sin)
* [`tan`](kcl/std-math-tan) * [`tan`](kcl/std-math-tan)
* **std::sketch** * **std::sketch**

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
--- ---
title: "KCL Known Issues" title: "KCL Known Issues"
excerpt: "Known issues with the KCL standard library for the Zoo Design Studio." excerpt: "Known issues with the KCL standard library for the Zoo Modeling App."
layout: manual layout: manual
--- ---

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
--- ---
title: "KCL Modules" title: "KCL Modules"
excerpt: "Documentation of modules for the KCL language for the Zoo Design Studio." excerpt: "Documentation of modules for the KCL language for the Zoo Modeling App."
layout: manual layout: manual
--- ---
@ -95,7 +95,7 @@ import "tests/inputs/cube.obj"
When importing a GLTF file, the bin file will be imported as well. When importing a GLTF file, the bin file will be imported as well.
Import paths are relative to the current project directory. Imports currently only work when Import paths are relative to the current project directory. Imports currently only work when
using the native Design Studio, not in the browser. using the native Modeling App, not in the browser.
### Supported values ### Supported values

43
docs/kcl/polar.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,12 @@
--- ---
title: "KCL Settings" title: "KCL Settings"
excerpt: "Documentation of settings for the KCL language and Zoo Design Studio." excerpt: "Documentation of settings for the KCL language and Zoo Modeling App."
layout: manual layout: manual
--- ---
# KCL Settings # KCL Settings
There are three levels of settings available in Zoo Design Studio: There are three levels of settings available in the KittyCAD modeling application:
1. [User Settings](/docs/kcl/settings/user): Global settings that apply to all projects, stored in `user.toml` 1. [User Settings](/docs/kcl/settings/user): Global settings that apply to all projects, stored in `user.toml`
2. [Project Settings](/docs/kcl/settings/project): Settings specific to a project, stored in `project.toml` 2. [Project Settings](/docs/kcl/settings/project): Settings specific to a project, stored in `project.toml`
@ -14,7 +14,7 @@ There are three levels of settings available in Zoo Design Studio:
## Configuration Files ## Configuration Files
Zoo Design Studio uses TOML files for configuration: The KittyCAD modeling app uses TOML files for configuration:
* **User Settings**: `user.toml` - See [complete documentation](/docs/kcl/settings/user) * **User Settings**: `user.toml` - See [complete documentation](/docs/kcl/settings/user)
* **Project Settings**: `project.toml` - See [complete documentation](/docs/kcl/settings/project) * **Project Settings**: `project.toml` - See [complete documentation](/docs/kcl/settings/project)

View File

@ -35,7 +35,7 @@ base_unit = "in"
#### app #### app
The settings for the Design Studio. The settings for the modeling app.
**Default:** None **Default:** None

View File

@ -37,7 +37,7 @@ text_wrapping = false
#### app #### app
The settings for the Design Studio. The settings for the modeling app.
**Default:** None **Default:** None
@ -96,7 +96,7 @@ Permanently dismiss the banner warning to download the desktop app. This setting
##### stream_idle_mode ##### stream_idle_mode
When the user is idle, teardown the stream after some time. When the user is idle, and this is true, the stream will be torn down.
**Default:** None **Default:** None

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@ circle(@sketch_or_surface: Sketch | Plane | Face, center: Point2d, radius: numbe
| Name | Type | Description | Required | | Name | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `sketch_or_surface` | [`Sketch`](/docs/kcl/types/Sketch) OR [`Plane`](/docs/kcl/types/Plane) OR [`Face`](/docs/kcl/types/Face) | Sketch to extend, or plane or surface to sketch on. | Yes | | `sketch_or_surface` | [`Sketch`](/docs/kcl/types/Sketch) `|` [`Plane`](/docs/kcl/types/Face) `|` [`Plane`](/docs/kcl/types/Face) | Sketch to extend, or plane or surface to sketch on. | Yes |
| `center` | [`Point2d`](/docs/kcl/types/Point2d) | The center of the circle. | Yes | | `center` | [`Point2d`](/docs/kcl/types/Point2d) | The center of the circle. | Yes |
| `radius` | [`number`](/docs/kcl/types/number) | The radius of the circle. | Yes | | `radius` | [`number`](/docs/kcl/types/number) | The radius of the circle. | Yes |
| [`tag`](/docs/kcl/types/tag) | [`tag`](/docs/kcl/types/tag) | Create a new tag which refers to this circle. | No | | [`tag`](/docs/kcl/types/tag) | [`tag`](/docs/kcl/types/tag) | Create a new tag which refers to this circle. | No |

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
--- ---
title: "KCL Types" title: "KCL Types"
excerpt: "Documentation of types for the KCL standard library for the Zoo Design Studio." excerpt: "Documentation of types for the KCL standard library for the Zoo Modeling App."
layout: manual layout: manual
--- ---

View File

@ -1,10 +1,10 @@
--- ---
title: "std::Axis2d" title: "std::Axis2d"
excerpt: "An infinite line in 2d space." excerpt: "An infinte line in 2d space."
layout: manual layout: manual
--- ---
An infinite line in 2d space. An infinte line in 2d space.

View File

@ -1,10 +1,10 @@
--- ---
title: "std::Axis3d" title: "std::Axis3d"
excerpt: "An infinite line in 3d space." excerpt: "An infinte line in 3d space."
layout: manual layout: manual
--- ---
An infinite line in 3d space. An infinte line in 3d space.

View File

@ -28,7 +28,7 @@ An extrude plane.
| `faceId` |[`string`](/docs/kcl/types/string)| The face id for the extrude plane. | No | | `faceId` |[`string`](/docs/kcl/types/string)| The face id for the extrude plane. | No |
| [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag. | No | | [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag. | No |
| `id` |[`string`](/docs/kcl/types/string)| The id of the geometry. | No | | `id` |[`string`](/docs/kcl/types/string)| The id of the geometry. | No |
| `sourceRange` |`[integer, integer, integer]`| The source range. | No | | `sourceRange` |[`SourceRange`](/docs/kcl/types/SourceRange)| The source range. | No |
---- ----
@ -48,7 +48,7 @@ An extruded arc.
| `faceId` |[`string`](/docs/kcl/types/string)| The face id for the extrude plane. | No | | `faceId` |[`string`](/docs/kcl/types/string)| The face id for the extrude plane. | No |
| [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag. | No | | [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag. | No |
| `id` |[`string`](/docs/kcl/types/string)| The id of the geometry. | No | | `id` |[`string`](/docs/kcl/types/string)| The id of the geometry. | No |
| `sourceRange` |`[integer, integer, integer]`| The source range. | No | | `sourceRange` |[`SourceRange`](/docs/kcl/types/SourceRange)| The source range. | No |
---- ----
@ -68,7 +68,7 @@ Geometry metadata.
| `faceId` |[`string`](/docs/kcl/types/string)| The id for the chamfer surface. | No | | `faceId` |[`string`](/docs/kcl/types/string)| The id for the chamfer surface. | No |
| [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag. | No | | [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag. | No |
| `id` |[`string`](/docs/kcl/types/string)| The id of the geometry. | No | | `id` |[`string`](/docs/kcl/types/string)| The id of the geometry. | No |
| `sourceRange` |`[integer, integer, integer]`| The source range. | No | | `sourceRange` |[`SourceRange`](/docs/kcl/types/SourceRange)| The source range. | No |
---- ----
@ -88,7 +88,7 @@ Geometry metadata.
| `faceId` |[`string`](/docs/kcl/types/string)| The id for the fillet surface. | No | | `faceId` |[`string`](/docs/kcl/types/string)| The id for the fillet surface. | No |
| [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag. | No | | [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag. | No |
| `id` |[`string`](/docs/kcl/types/string)| The id of the geometry. | No | | `id` |[`string`](/docs/kcl/types/string)| The id of the geometry. | No |
| `sourceRange` |`[integer, integer, integer]`| The source range. | No | | `sourceRange` |[`SourceRange`](/docs/kcl/types/SourceRange)| The source range. | No |
---- ----

View File

@ -17,6 +17,6 @@ Geometry metadata.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `id` |[`string`](/docs/kcl/types/string)| The id of the geometry. | No | | `id` |[`string`](/docs/kcl/types/string)| The id of the geometry. | No |
| `sourceRange` |`[integer, integer, integer]`| The source range. | No | | `sourceRange` |[`SourceRange`](/docs/kcl/types/SourceRange)| The source range. | No |

View File

@ -17,7 +17,7 @@ A helix.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `value` |[`string`](/docs/kcl/types/string)| The id of the helix. | No | | `value` |[`string`](/docs/kcl/types/string)| The id of the helix. | No |
| `artifactId` |[`string`](/docs/kcl/types/string)| The artifact ID. | No | | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No |
| `revolutions` |[`number`](/docs/kcl/types/number)| Number of revolutions. | No | | `revolutions` |[`number`](/docs/kcl/types/number)| Number of revolutions. | No |
| `angleStart` |[`number`](/docs/kcl/types/number)| Start angle (in degrees). | No | | `angleStart` |[`number`](/docs/kcl/types/number)| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No | | `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |

View File

@ -285,7 +285,7 @@ Data for an imported geometry.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Module`| | No | | `type` |enum: `Module`| | No |
| `value` |`integer`| Identifier of a source file. Uses a u32 to keep the size small. | No | | `value` |[`ModuleId`](/docs/kcl/types/ModuleId)| Identifier of a source file. Uses a u32 to keep the size small. | No |
---- ----

View File

@ -0,0 +1,22 @@
---
title: "PolarCoordsData"
excerpt: "Data for polar coordinates."
layout: manual
---
Data for polar coordinates.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `angle` |[`number`](/docs/kcl/types/number)| The angle of the line (in degrees). | No |
| `length` |[`TyF64`](/docs/kcl/types/TyF64)| The length of the line. | No |

View File

@ -25,7 +25,7 @@ A sketch type.
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `plane`| | No | | `type` |enum: `plane`| | No |
| `id` |[`string`](/docs/kcl/types/string)| The id of the plane. | No | | `id` |[`string`](/docs/kcl/types/string)| The id of the plane. | No |
| `artifactId` |[`string`](/docs/kcl/types/string)| The artifact ID. | No | | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Type for a plane. | No | | `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Type for a plane. | No |
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's X axis be? | No | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's X axis be? | No |
@ -49,7 +49,7 @@ A face.
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `face`| | No | | `type` |enum: `face`| | No |
| `id` |[`string`](/docs/kcl/types/string)| The id of the face. | No | | `id` |[`string`](/docs/kcl/types/string)| The id of the face. | No |
| `artifactId` |[`string`](/docs/kcl/types/string)| The artifact ID. | No | | `artifactId` |[`ArtifactId`](/docs/kcl/types/ArtifactId)| The artifact ID. | No |
| `value` |[`string`](/docs/kcl/types/string)| The tag of the face. | No | | `value` |[`string`](/docs/kcl/types/string)| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's X axis be? | No | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's Y axis be? | No | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's Y axis be? | No |

View File

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

View File

@ -5,11 +5,17 @@ layout: manual
--- ---
**Type:** [`number`](/docs/kcl/types/number) (`double`) **Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `n` |[`number`](/docs/kcl/types/number)| | No |
| `ty` |[`NumericType`](/docs/kcl/types/NumericType)| | No |

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import { expect, test } from '@e2e/playwright/zoo-test' import { test, expect } from './zoo-test'
test.describe('Electron app header tests', () => { test.describe('Electron app header tests', () => {
test( test(

View File

@ -1,14 +1,13 @@
import type { Page } from '@playwright/test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import type { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture'
import { import {
PERSIST_MODELING_CONTEXT, getUtils,
TEST_COLORS, TEST_COLORS,
commonPoints, commonPoints,
getUtils, PERSIST_MODELING_CONTEXT,
orRunWhenFullSuiteEnabled, orRunWhenFullSuiteEnabled,
} from '@e2e/playwright/test-utils' } from './test-utils'
import { expect, test } from '@e2e/playwright/zoo-test' import { HomePageFixture } from './fixtures/homePageFixture'
test.setTimeout(120000) test.setTimeout(120000)
@ -86,7 +85,7 @@ async function doBasicSketch(
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(u.codeLocator) await expect(u.codeLocator)
.toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${ .toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
commonPoints.startAt commonPoints.startAt
}, sketch001) }, sketch001)
|> xLine(length = ${commonPoints.num1}) |> xLine(length = ${commonPoints.num1})
@ -120,7 +119,10 @@ async function doBasicSketch(
await page.waitForTimeout(100) await page.waitForTimeout(100)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
expect(await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)).toBeLessThan(3) await expect(
await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)
).toBeLessThan(3)
await expect(await u.getGreatestPixDiff(line1, [0, 0, 255])).toBeLessThan(3)
} }
// hold down shift // hold down shift
@ -143,7 +145,7 @@ async function doBasicSketch(
// Open the code pane. // Open the code pane.
await u.openKclCodePanel() await u.openKclCodePanel()
await expect(u.codeLocator) await expect(u.codeLocator)
.toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${ .toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
commonPoints.startAt commonPoints.startAt
}, sketch001) }, sketch001)
|> xLine(length = ${commonPoints.num1}, tag = $seg01) |> xLine(length = ${commonPoints.num1}, tag = $seg01)

View File

@ -1,119 +0,0 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Point and click for boolean workflows', () => {
// Boolean operations to test
const booleanOperations = [
{
name: 'union',
code: 'union([extrude001, extrude006])',
},
{
name: 'subtract',
code: 'subtract([extrude001], tools = [extrude006])',
},
{
name: 'intersect',
code: 'intersect([extrude001, extrude006])',
},
] as const
for (let i = 0; i < booleanOperations.length; i++) {
const operation = booleanOperations[i]
const operationName = operation.name
const commandName = `Boolean ${
operationName.charAt(0).toUpperCase() + operationName.slice(1)
}`
test(`Create boolean operation -- ${operationName}`, async ({
context,
homePage,
cmdBar,
editor,
toolbar,
scene,
page,
}) => {
const file = await fs.readFile(
path.resolve(
__dirname,
'../../',
'./rust/kcl-lib/e2e/executor/inputs/boolean-setup-with-sketch-on-faces.kcl'
),
'utf-8'
)
await context.addInitScript((file) => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.settled(cmdBar)
// Test coordinates for selection - these might need adjustment based on actual scene layout
const cylinderPoint = { x: 592, y: 174 }
const secondObjectPoint = { x: 683, y: 273 }
// Create mouse helpers for selecting objects
const [clickFirstObject] = scene.makeMouseHelpers(
cylinderPoint.x,
cylinderPoint.y,
{ steps: 10 }
)
const [clickSecondObject] = scene.makeMouseHelpers(
secondObjectPoint.x,
secondObjectPoint.y,
{ steps: 10 }
)
await test.step(`Test ${operationName} operation`, async () => {
// Click the boolean operation button in the toolbar
await toolbar.selectBoolean(operationName)
// Verify command bar is showing the right command
await expect(cmdBar.page.getByTestId('command-name')).toContainText(
commandName
)
// Select first object in the scene, expect there to be a pixel diff from the selection color change
await clickFirstObject({ pixelDiff: 50 })
await page.waitForTimeout(1000)
// For subtract, we need to proceed to the next step before selecting the second object
if (operationName !== 'subtract') {
// should down shift key to select multiple objects
await page.keyboard.down('Shift')
}
// Select second object
await clickSecondObject({ pixelDiff: 50 })
await page.waitForTimeout(1000)
// Confirm the operation in the command bar
await cmdBar.progressCmdBar()
if (operationName === 'union' || operationName === 'intersect') {
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Solids: '2 paths',
},
commandName,
})
} else if (operationName === 'subtract') {
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Tool: '1 path',
Target: '1 path',
},
commandName,
})
}
await cmdBar.submit()
await editor.expectEditor.toContain(operation.code)
})
})
}
})

View File

@ -1,12 +1,10 @@
import type { Page } from '@playwright/test' import { Page } from '@playwright/test'
import type { EngineCommand } from '@src/lang/std/artifactGraph' import { test, expect } from './zoo-test'
import { uuidv4 } from '@src/lib/utils' import { HomePageFixture } from './fixtures/homePageFixture'
import { getUtils } from './test-utils'
import type { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture' import { EngineCommand } from 'lang/std/artifactGraph'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture' import { uuidv4 } from 'lib/utils'
import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture' import { SceneFixture } from './fixtures/sceneFixture'
import { getUtils } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe( test.describe(
'Can create sketches on all planes and their back sides', 'Can create sketches on all planes and their back sides',
@ -16,7 +14,6 @@ test.describe(
page: Page, page: Page,
homePage: HomePageFixture, homePage: HomePageFixture,
scene: SceneFixture, scene: SceneFixture,
toolbar: ToolbarFixture,
plane: string, plane: string,
clickCoords: { x: number; y: number } clickCoords: { x: number; y: number }
) => { ) => {
@ -49,7 +46,7 @@ test.describe(
}, },
} }
const code = `@settings(defaultLengthUnit = in)sketch001 = startSketchOn(${plane})profile001 = startProfileAt([0.91, -1.22], sketch001)` const code = `sketch001 = startSketchOn(${plane})profile001 = startProfileAt([0.91, -1.22], sketch001)`
await u.openDebugPanel() await u.openDebugPanel()
@ -61,12 +58,9 @@ test.describe(
await u.sendCustomCmd(updateCamCommand) await u.sendCustomCmd(updateCamCommand)
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(clickCoords.x, clickCoords.y) await page.mouse.click(clickCoords.x, clickCoords.y)
await page.waitForTimeout(600) // wait for animation await page.waitForTimeout(600) // wait for animation
await toolbar.waitUntilSketchingReady()
await expect( await expect(
page.getByRole('button', { name: 'line Line', exact: true }) page.getByRole('button', { name: 'line Line', exact: true })
).toBeVisible() ).toBeVisible()
@ -122,12 +116,11 @@ test.describe(
] ]
for (const config of planeConfigs) { for (const config of planeConfigs) {
test(config.plane, async ({ page, homePage, scene, toolbar }) => { test(config.plane, async ({ page, homePage, scene }) => {
await sketchOnPlaneAndBackSideTest( await sketchOnPlaneAndBackSideTest(
page, page,
homePage, homePage,
scene, scene,
toolbar,
config.plane, config.plane,
config.coords config.coords
) )

View File

@ -1,21 +1,19 @@
import { bracket } from '@src/lib/exampleKcl' import { test, expect } from './zoo-test'
import fsp from 'fs/promises'
import { join } from 'path'
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from '@e2e/playwright/storageStates'
import { import {
executorInputPath,
getUtils,
orRunWhenFullSuiteEnabled, orRunWhenFullSuiteEnabled,
} from '@e2e/playwright/test-utils' getUtils,
import { expect, test } from '@e2e/playwright/zoo-test' 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.describe('Code pane and errors', { tag: ['@skipWin'] }, () => { test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
test('Typing KCL errors induces a badge on the code pane button', async ({ test('Typing KCL errors induces a badge on the code pane button', async ({
page, page,
homePage, homePage,
scene, scene,
cmdBar,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -37,7 +35,7 @@ extrude001 = extrude(sketch001, length = 5)`
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await scene.waitForExecutionDone()
// Ensure no badge is present // Ensure no badge is present
const codePaneButtonHolder = page.locator('#code-button-holder') const codePaneButtonHolder = page.locator('#code-button-holder')
@ -172,8 +170,6 @@ extrude001 = extrude(sketch001, length = 5)`
context, context,
page, page,
homePage, homePage,
scene,
cmdBar,
}) => { }) => {
// Load the app with the working starter code // Load the app with the working starter code
await context.addInitScript((code) => { await context.addInitScript((code) => {
@ -183,7 +179,9 @@ extrude001 = extrude(sketch001, length = 5)`
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) // FIXME: await scene.waitForExecutionDone() does not work. It still fails.
// I needed to increase this timeout to get this to pass.
await page.waitForTimeout(10000)
// Ensure badge is present // Ensure badge is present
const codePaneButtonHolder = page.locator('#code-button-holder') const codePaneButtonHolder = page.locator('#code-button-holder')
@ -253,11 +251,11 @@ test(
]) ])
await Promise.all([ await Promise.all([
fsp.copyFile( fsp.copyFile(
executorInputPath('cylinder-inches.kcl'), executorInputPath('router-template-slate.kcl'),
join(routerTemplateDir, 'main.kcl') join(routerTemplateDir, 'main.kcl')
), ),
fsp.copyFile( fsp.copyFile(
executorInputPath('e2e-can-sketch-on-chamfer.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl') join(bracketDir, 'main.kcl')
), ),
]) ])

View File

@ -1,13 +1,12 @@
import { KCL_DEFAULT_LENGTH } from '@src/lib/constants' import { test, expect } from './zoo-test'
import * as fsp from 'fs/promises' import * as fsp from 'fs/promises'
import path, { join } from 'path'
import { import {
executorInputPath, executorInputPath,
getUtils, getUtils,
orRunWhenFullSuiteEnabled, orRunWhenFullSuiteEnabled,
} from '@e2e/playwright/test-utils' } from './test-utils'
import { expect, test } from '@e2e/playwright/zoo-test' import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import path, { join } from 'path'
test.describe('Command bar tests', { tag: ['@skipWin'] }, () => { test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
test('Extrude from command bar selects extrude line after', async ({ test('Extrude from command bar selects extrude line after', async ({
@ -317,13 +316,9 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
test('Can switch between sketch tools via command bar', async ({ test('Can switch between sketch tools via command bar', async ({
page, page,
homePage, homePage,
scene,
cmdBar,
toolbar,
}) => { }) => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar)
const sketchButton = page.getByRole('button', { name: 'Start Sketch' }) const sketchButton = page.getByRole('button', { name: 'Start Sketch' })
const cmdBarButton = page.getByRole('button', { name: 'Commands' }) const cmdBarButton = page.getByRole('button', { name: 'Commands' })
@ -347,9 +342,7 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
// Start a sketch // Start a sketch
await sketchButton.click() await sketchButton.click()
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await toolbar.waitUntilSketchingReady()
// Switch between sketch tools via the command bar // Switch between sketch tools via the command bar
await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true') await expect(lineToolButton).toHaveAttribute('aria-pressed', 'true')

View File

@ -1,5 +1,5 @@
import { getUtils } from '@e2e/playwright/test-utils' import { test, expect } from './zoo-test'
import { expect, test } from '@e2e/playwright/zoo-test' import { getUtils } from './test-utils'
test.describe('Copilot ghost text', () => { test.describe('Copilot ghost text', () => {
// eslint-disable-next-line jest/valid-title // eslint-disable-next-line jest/valid-title

View File

@ -1,5 +1,6 @@
import { getUtils } from '@e2e/playwright/test-utils' import { test, expect } from './zoo-test'
import { expect, test } from '@e2e/playwright/zoo-test'
import { getUtils } from './test-utils'
function countNewlines(input: string): number { function countNewlines(input: string): number {
let count = 0 let count = 0

View File

@ -1,17 +1,16 @@
import fsp from 'fs/promises' import { test, expect } from './zoo-test'
import path from 'path' import path from 'path'
import { import {
getUtils,
executorInputPath, executorInputPath,
getPlaywrightDownloadDir, getPlaywrightDownloadDir,
getUtils, } from './test-utils'
} from '@e2e/playwright/test-utils' import fsp from 'fs/promises'
import { expect, test } from '@e2e/playwright/zoo-test'
test( test(
'export works on the first try', 'export works on the first try',
{ tag: ['@electron', '@skipLocalEngine'] }, { tag: ['@electron', '@skipLocalEngine'] },
async ({ page, context, scene, tronApp, cmdBar }, testInfo) => { async ({ page, context, scene, tronApp }, testInfo) => {
if (!tronApp) { if (!tronApp) {
fail() fail()
} }
@ -21,52 +20,68 @@ test(
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })]) await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
await Promise.all([ await Promise.all([
fsp.copyFile( fsp.copyFile(
executorInputPath('cylinder-inches.kcl'), executorInputPath('router-template-slate.kcl'),
path.join(bracketDir, 'other.kcl') path.join(bracketDir, 'other.kcl')
), ),
fsp.copyFile( fsp.copyFile(
executorInputPath('e2e-can-sketch-on-chamfer.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
path.join(bracketDir, 'main.kcl') path.join(bracketDir, 'main.kcl')
), ),
]) ])
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await test.step('on open of project', async () => { page.on('console', console.log)
// Open the project
const projectName = page.getByText(`bracket`)
await expect(projectName).toBeVisible()
await projectName.click()
await scene.settled(cmdBar)
// Expect zero errors in gutter await test.step('on open of project', async () => {
await expect(page.getByText(`bracket`)).toBeVisible()
// open the project
await page.getByText(`bracket`).click()
// expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Click the export button // export the model
const exportButton = page.getByTestId('export-pane-button') const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible() await expect(exportButton).toBeVisible()
// Wait for the model to finish loading
const modelStateIndicator = page.getByTestId(
'model-state-indicator-execution-done'
)
await expect(modelStateIndicator).toBeVisible({ timeout: 60000 })
const gltfOption = page.getByText('glTF')
const submitButton = page.getByText('Confirm Export')
const exportingToastMessage = page.getByText(`Exporting...`)
const errorToastMessage = page.getByText(`Error while exporting`)
const engineErrorToastMessage = page.getByText(`Nothing to export`)
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
// The open file's name is `main.kcl`, so the export file name should be `main.gltf`
const exportFileName = `main.gltf`
// Click the export button
await exportButton.click() await exportButton.click()
// Select the first format option
const gltfOption = cmdBar.selectOption({ name: 'glTF' })
const exportFileName = `main.gltf` // source file is named `main.kcl`
await expect(gltfOption).toBeVisible() await expect(gltfOption).toBeVisible()
await expect(page.getByText('STL')).toBeVisible()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// Click the checkbox // Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible() await expect(submitButton).toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// Find the toast.
// Look out for the toast message // Look out for the toast message
const exportingToastMessage = page.getByText(`Exporting...`)
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
await expect(exportingToastMessage).toBeVisible() await expect(exportingToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible() await expect(alreadyExportingToastMessage).not.toBeVisible()
// Expect it to succeed // Expect it to succeed.
const errorToastMessage = page.getByText(`Error while exporting`)
const engineErrorToastMessage = page.getByText(`Nothing to export`)
await expect(errorToastMessage).not.toBeVisible() await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible() await expect(engineErrorToastMessage).not.toBeVisible()
@ -74,7 +89,6 @@ test(
await expect(successToastMessage).toBeVisible() await expect(successToastMessage).toBeVisible()
await expect(exportingToastMessage).not.toBeVisible() await expect(exportingToastMessage).not.toBeVisible()
// Check for the exported file
const firstFileFullPath = path.resolve( const firstFileFullPath = path.resolve(
getPlaywrightDownloadDir(tronApp.projectDirName), getPlaywrightDownloadDir(tronApp.projectDirName),
exportFileName exportFileName
@ -93,7 +107,7 @@ test(
}, },
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBeGreaterThan(30_000) .toBeGreaterThan(300_000)
}) })
}) })
@ -101,50 +115,60 @@ test(
const u = await getUtils(page) const u = await getUtils(page)
await u.openFilePanel() await u.openFilePanel()
// Click on the other file
const otherKclButton = page.getByRole('button', { name: 'other.kcl' }) const otherKclButton = page.getByRole('button', { name: 'other.kcl' })
// Click the file
await otherKclButton.click() await otherKclButton.click()
// Close the file pane // Close the file pane
await u.closeFilePanel() await u.closeFilePanel()
await scene.settled(cmdBar)
// Expect zero errors in gutter // FIXME: await scene.waitForExecutionDone() does not work. The modeling indicator stays in -receive-reliable and not execution done
await page.waitForTimeout(10000)
// expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Click the export button // export the model
const exportButton = page.getByTestId('export-pane-button') const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible() await expect(exportButton).toBeVisible()
const gltfOption = page.getByText('glTF')
const submitButton = page.getByText('Confirm Export')
const exportingToastMessage = page.getByText(`Exporting...`)
const errorToastMessage = page.getByText(`Error while exporting`)
const engineErrorToastMessage = page.getByText(`Nothing to export`)
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
// The open file's name is `other.kcl`, so the export file name should be `other.gltf`
const exportFileName = `other.gltf`
// Click the export button
await exportButton.click() await exportButton.click()
// Select the first format option
const gltfOption = cmdBar.selectOption({ name: 'glTF' })
const exportFileName = `other.gltf` // source file is named `other.kcl`
await expect(gltfOption).toBeVisible() await expect(gltfOption).toBeVisible()
await expect(page.getByText('STL')).toBeVisible()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// Click the checkbox // Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible() await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// Find the toast.
// Look out for the toast message // Look out for the toast message
const exportingToastMessage = page.getByText(`Exporting...`)
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
await expect(exportingToastMessage).toBeVisible() await expect(exportingToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible()
// Expect it to succeed
const errorToastMessage = page.getByText(`Error while exporting`)
const engineErrorToastMessage = page.getByText(`Nothing to export`)
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
const successToastMessage = page.getByText(`Exported successfully`) const successToastMessage = page.getByText(`Exported successfully`)
await expect(successToastMessage).toBeVisible() await test.step('Check the success toast message shows and nothing else', async () =>
await expect(exportingToastMessage).not.toBeVisible() Promise.all([
expect(alreadyExportingToastMessage).not.toBeVisible(),
expect(errorToastMessage).not.toBeVisible(),
expect(engineErrorToastMessage).not.toBeVisible(),
expect(successToastMessage).toBeVisible(),
expect(exportingToastMessage).not.toBeVisible(),
]))
// Check for the exported file=
const secondFileFullPath = path.resolve( const secondFileFullPath = path.resolve(
getPlaywrightDownloadDir(tronApp.projectDirName), getPlaywrightDownloadDir(tronApp.projectDirName),
exportFileName exportFileName
@ -163,7 +187,7 @@ test(
}, },
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBeGreaterThan(50_000) .toBeGreaterThan(70_000)
}) })
}) })
} }

View File

@ -1,14 +1,14 @@
import { uuidv4 } from '@src/lib/utils' import { test, expect } from './zoo-test'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { join } from 'path' import { uuidv4 } from 'lib/utils'
import { import {
TEST_COLORS,
executorInputPath, executorInputPath,
getUtils, getUtils,
orRunWhenFullSuiteEnabled, orRunWhenFullSuiteEnabled,
} from '@e2e/playwright/test-utils' TEST_COLORS,
import { expect, test } from '@e2e/playwright/zoo-test' } from './test-utils'
import { join } from 'path'
test.describe('Editor tests', { tag: ['@skipWin'] }, () => { test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
test('can comment out code with ctrl+/', async ({ page, homePage }) => { test('can comment out code with ctrl+/', async ({ page, homePage }) => {
@ -32,30 +32,26 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
await page.keyboard.press('/') await page.keyboard.press('/')
await page.keyboard.up('ControlOrMeta') await page.keyboard.up('ControlOrMeta')
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content'))
`@settings(defaultLengthUnit = in) .toHaveText(`sketch001 = startSketchOn(XY)
sketch001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line(end = [20, 0]) |> line(end = [20, 0])
|> line(end = [0, 20]) |> line(end = [0, 20])
|> line(end = [-20, 0]) |> line(end = [-20, 0])
// |> close()`.replaceAll('\n', '') // |> close()`)
)
// uncomment the code // uncomment the code
await page.keyboard.down('ControlOrMeta') await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('/') await page.keyboard.press('/')
await page.keyboard.up('ControlOrMeta') await page.keyboard.up('ControlOrMeta')
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content'))
`@settings(defaultLengthUnit = in) .toHaveText(`sketch001 = startSketchOn(XY)
sketch001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line(end = [20, 0]) |> line(end = [20, 0])
|> line(end = [0, 20]) |> line(end = [0, 20])
|> line(end = [-20, 0]) |> line(end = [-20, 0])
|> close()`.replaceAll('\n', '') |> close()`)
)
}) })
test('ensure we use the cache, and do not re-execute', async ({ test('ensure we use the cache, and do not re-execute', async ({
@ -78,14 +74,12 @@ sketch001 = startSketchOn(XY)
// Ensure we execute the first time. // Ensure we execute the first time.
await u.openDebugPanel() await u.openDebugPanel()
await expect await expect(
.poll(() => page.locator('[data-receive-command-type="scene_clear_all"]')
page.locator('[data-receive-command-type="scene_clear_all"]').count() ).toHaveCount(1)
) await expect(
.toBe(1) page.locator('[data-message-type="execution-done"]')
await expect ).toHaveCount(2)
.poll(() => page.locator('[data-message-type="execution-done"]').count())
.toBe(2)
// Add whitespace to the end of the code. // Add whitespace to the end of the code.
await u.codeLocator.click() await u.codeLocator.click()
@ -112,14 +106,12 @@ sketch001 = startSketchOn(XY)
test('ensure we use the cache, and do not clear on append', async ({ test('ensure we use the cache, and do not clear on append', async ({
homePage, homePage,
page, page,
scene,
cmdBar,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await u.waitForPageLoad()
await u.codeLocator.click() await u.codeLocator.click()
await page.keyboard.type(`sketch001 = startSketchOn(XY) await page.keyboard.type(`sketch001 = startSketchOn(XY)
@ -186,15 +178,13 @@ sketch001 = startSketchOn(XY)
await page.locator('#code-pane button:first-child').click() await page.locator('#code-pane button:first-child').click()
await page.locator('button:has-text("Format code")').click() await page.locator('button:has-text("Format code")').click()
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content'))
`@settings(defaultLengthUnit = in) .toHaveText(`sketch001 = startSketchOn(XY)
sketch001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line(end = [20, 0]) |> line(end = [20, 0])
|> line(end = [0, 20]) |> line(end = [0, 20])
|> line(end = [-20, 0]) |> line(end = [-20, 0])
|> close()`.replaceAll('\n', '') |> close()`)
)
}) })
test('if you click the format button it formats your code and executes so lints are still there', async ({ test('if you click the format button it formats your code and executes so lints are still there', async ({
@ -237,15 +227,13 @@ sketch001 = startSketchOn(XY)
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content'))
`@settings(defaultLengthUnit = in) .toHaveText(`sketch_001 = startSketchOn(XY)
sketch_001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line(end = [20, 0]) |> line(end = [20, 0])
|> line(end = [0, 20]) |> line(end = [0, 20])
|> line(end = [-20, 0]) |> line(end = [-20, 0])
|> close()`.replaceAll('\n', '') |> close()`)
)
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
@ -483,7 +471,6 @@ sketch_001 = startSketchOn(XY)
test('if you write kcl with lint errors you get lints', async ({ test('if you write kcl with lint errors you get lints', async ({
page, page,
homePage, homePage,
scene,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
@ -503,7 +490,10 @@ sketch_001 = startSketchOn(XY)
await page.keyboard.press('ArrowLeft') await page.keyboard.press('ArrowLeft')
await page.keyboard.press('ArrowRight') await page.keyboard.press('ArrowRight')
await scene.connectionEstablished() // FIXME: lsp errors do not propagate to the frontend until engine is connected and code is executed
// This timeout is to wait for engine connection. LSP and code execution errors should be handled differently
// LSP can emit errors as fast as it waits and show them in the editor
await page.waitForTimeout(10000)
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
@ -825,12 +815,10 @@ sketch_001 = startSketchOn(XY)
// there shouldn't be any auto complete options for 'lin' in the comment // there shouldn't be any auto complete options for 'lin' in the comment
await expect(page.locator('.cm-completionLabel')).not.toBeVisible() await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content'))
`@settings(defaultLengthUnit = in) .toHaveText(`sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
|> startProfileAt([3.14, 12], %) |> startProfileAt([3.14, 12], %)
|> xLine(%, length = 5) // lin`.replaceAll('\n', '') |> xLine(%, length = 5) // lin`)
)
// expect there to be no KCL errors // expect there to be no KCL errors
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0) await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0)
@ -900,12 +888,10 @@ sketch001 = startSketchOn(XZ)
// there shouldn't be any auto complete options for 'lin' in the comment // there shouldn't be any auto complete options for 'lin' in the comment
await expect(page.locator('.cm-completionLabel')).not.toBeVisible() await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content'))
`@settings(defaultLengthUnit = in) .toHaveText(`sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
|> startProfileAt([3.14, 12], %) |> startProfileAt([3.14, 12], %)
|> xLine(%, length = 5) // lin`.replaceAll('\n', '') |> xLine(%, length = 5) // lin`)
)
}) })
}) })
test('Can undo a click and point extrude with ctrl+z', async ({ test('Can undo a click and point extrude with ctrl+z', async ({
@ -989,13 +975,12 @@ sketch001 = startSketchOn(XZ)
test( test(
'Can undo a sketch modification with ctrl+z', 'Can undo a sketch modification with ctrl+z',
{ tag: ['@skipWin'] }, { tag: ['@skipWin'] },
async ({ page, homePage, editor }) => { async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit=in) `sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
|> startProfileAt([4.61, -10.01], %) |> startProfileAt([4.61, -10.01], %)
|> line(end = [12.73, -0.09]) |> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
@ -1085,45 +1070,41 @@ sketch001 = startSketchOn(XZ)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed // expect the code to have changed
await editor.expectEditor.toContain( await expect(page.locator('.cm-content'))
`sketch001 = startSketchOn(XZ) .toHaveText(`sketch001 = startSketchOn(XZ)
|> startProfileAt([2.71, -2.71], %) |> startProfileAt([2.71, -2.71], %)
|> line(end = [15.4, -2.78]) |> line(end = [15.4, -2.78])
|> tangentialArcTo([27.6, -3.05], %) |> tangentialArcTo([27.6, -3.05], %)
|> close() |> close()
|> extrude(length = 5)`, |> extrude(length = 5)
{ shouldNormalise: true } `)
)
// Hit undo // Hit undo
await page.keyboard.down('Control') await page.keyboard.down('Control')
await page.keyboard.press('KeyZ') await page.keyboard.press('KeyZ')
await page.keyboard.up('Control') await page.keyboard.up('Control')
await editor.expectEditor.toContain( await expect(page.locator('.cm-content'))
`sketch001 = startSketchOn(XZ) .toHaveText(`sketch001 = startSketchOn(XZ)
|> startProfileAt([2.71, -2.71], %) |> startProfileAt([2.71, -2.71], %)
|> line(end = [15.4, -2.78]) |> line(end = [15.4, -2.78])
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close() |> close()
|> extrude(length = 5)`, |> extrude(length = 5)`)
{ shouldNormalise: true }
)
// Hit undo again. // Hit undo again.
await page.keyboard.down('Control') await page.keyboard.down('Control')
await page.keyboard.press('KeyZ') await page.keyboard.press('KeyZ')
await page.keyboard.up('Control') await page.keyboard.up('Control')
await editor.expectEditor.toContain( await expect(page.locator('.cm-content'))
`sketch001 = startSketchOn(XZ) .toHaveText(`sketch001 = startSketchOn(XZ)
|> startProfileAt([2.71, -2.71], %) |> startProfileAt([2.71, -2.71], %)
|> line(end = [12.73, -0.09]) |> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close() |> close()
|> extrude(length = 5)`, |> extrude(length = 5)
{ shouldNormalise: true } `)
)
// Hit undo again. // Hit undo again.
await page.keyboard.down('Control') await page.keyboard.down('Control')
@ -1131,15 +1112,13 @@ sketch001 = startSketchOn(XZ)
await page.keyboard.up('Control') await page.keyboard.up('Control')
await page.waitForTimeout(100) await page.waitForTimeout(100)
await editor.expectEditor.toContain( await expect(page.locator('.cm-content'))
`sketch001 = startSketchOn(XZ) .toHaveText(`sketch001 = startSketchOn(XZ)
|> startProfileAt([4.61, -10.01], %) |> startProfileAt([4.61, -10.01], %)
|> line(end = [12.73, -0.09]) |> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close() |> close()
|> extrude(length = 5)`, |> extrude(length = 5)`)
{ shouldNormalise: true }
)
} }
) )
@ -1227,130 +1206,4 @@ sketch001 = startSketchOn(XZ)
}) })
} }
) )
test('Rectangle tool panning with middle click', async ({
page,
homePage,
toolbar,
scene,
cmdBar,
editor,
}) => {
await page.setBodyDimensions({ width: 1200, height: 900 })
await homePage.goToModelingScene()
// wait until scene is ready to be interacted with
await scene.connectionEstablished()
await scene.settled(cmdBar)
await page.getByRole('button', { name: 'Start Sketch' }).click()
// select an axis plane
await page.mouse.click(700, 200)
// Needed as we don't yet have a way to get a signal from the engine that the camera has animated to the sketch plane
await page.waitForTimeout(1000)
const middleMousePan = async (
startX: number,
startY: number,
endX: number,
endY: number
) => {
const initialCode = await editor.getCurrentCode()
await page.mouse.click(startX, startY, { button: 'middle' })
await page.mouse.move(endX, endY, {
steps: 10,
})
// We expect the code to be the same, middle mouse click should not modify the code, only do panning
await editor.expectEditor.toBe(initialCode)
}
await test.step(`Verify corner rectangle panning`, async () => {
await page.getByTestId('corner-rectangle').click()
await middleMousePan(800, 500, 900, 600)
})
await test.step(`Verify center rectangle panning`, async () => {
await toolbar.selectCenterRectangle()
await middleMousePan(800, 200, 900, 300)
})
})
test('Can select lines on the main axis', async ({
page,
homePage,
toolbar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([100.00, 100.0], sketch001)
|> yLine(length = -100.0)
|> xLine(length = 200.0)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`
)
})
const width = 1200
const height = 800
const viewportSize = { width, height }
await page.setBodyDimensions(viewportSize)
await homePage.goToModelingScene()
const u = await getUtils(page)
await u.waitForPageLoad()
await toolbar.editSketch(0)
await page.waitForTimeout(1000)
// Click on the bottom segment that lies on the x axis
await page.mouse.click(width * 0.85, height / 2)
await page.waitForTimeout(1000)
// Verify segment is selected (you can check for visual indicators or state)
const element = page.locator('[data-overlay-index="1"]')
await expect(element).toHaveAttribute('data-overlay-visible', 'true')
})
test(`Only show axis planes when there are no errors`, async ({
page,
homePage,
scene,
cmdBar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn(XZ)
profile001 = circle(sketch001, center = [-100.0, -100.0], radius = 50.0)
sketch002 = startSketchOn(XZ)
profile002 = circle(sketch002, center = [-100.0, 100.0], radius = 50.0)
extrude001 = extrude(profile002, length = 0)` // length = 0 is causing the error
)
})
const viewportSize = { width: 1200, height: 800 }
await page.setBodyDimensions(viewportSize)
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
await scene.expectPixelColor(
TEST_COLORS.DARK_MODE_BKGD,
// This is a position where the blue part of the axis plane is visible if its rendered
{ x: viewportSize.width * 0.75, y: viewportSize.height * 0.2 },
15
)
})
}) })

View File

@ -1,8 +1,7 @@
import { test, expect } from './zoo-test'
import * as fsp from 'fs/promises' import * as fsp from 'fs/promises'
import { join } from 'path' import { join } from 'path'
import { expect, test } from '@e2e/playwright/zoo-test'
const FEATURE_TREE_EXAMPLE_CODE = `export fn timesFive(x) { const FEATURE_TREE_EXAMPLE_CODE = `export fn timesFive(x) {
return 5 * x return 5 * x
} }
@ -64,7 +63,7 @@ test.describe('Feature Tree pane', () => {
test( test(
'User can go to definition and go to function definition', 'User can go to definition and go to function definition',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, homePage, scene, editor, toolbar, cmdBar, page }) => { async ({ context, homePage, scene, editor, toolbar }) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const bracketDir = join(dir, 'test-sample') const bracketDir = join(dir, 'test-sample')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
@ -86,13 +85,9 @@ test.describe('Feature Tree pane', () => {
sortBy: 'last-modified-desc', sortBy: 'last-modified-desc',
}) })
await homePage.openProject('test-sample') await homePage.openProject('test-sample')
await scene.connectionEstablished() await scene.waitForExecutionDone()
await scene.settled(cmdBar) await editor.closePane()
await toolbar.openFeatureTreePane() await toolbar.openFeatureTreePane()
await expect
.poll(() => page.getByText('Feature tree').count())
.toBeGreaterThan(1)
}) })
async function testViewSource({ async function testViewSource({
@ -258,7 +253,7 @@ test.describe('Feature Tree pane', () => {
sortBy: 'last-modified-desc', sortBy: 'last-modified-desc',
}) })
await homePage.openProject('test-sample') await homePage.openProject('test-sample')
await scene.settled(cmdBar) await scene.waitForExecutionDone()
await toolbar.openFeatureTreePane() await toolbar.openFeatureTreePane()
}) })
@ -343,7 +338,7 @@ test.describe('Feature Tree pane', () => {
sortBy: 'last-modified-desc', sortBy: 'last-modified-desc',
}) })
await homePage.openProject('test-sample') await homePage.openProject('test-sample')
await scene.settled(cmdBar) await scene.waitForExecutionDone()
await toolbar.openFeatureTreePane() await toolbar.openFeatureTreePane()
}) })
@ -418,7 +413,8 @@ profile003 = startProfileAt([0, -4.93], sketch001)
const planeColor: [number, number, number] = [74, 74, 74] const planeColor: [number, number, number] = [74, 74, 74]
await homePage.openProject('test-sample') await homePage.openProject('test-sample')
await scene.settled(cmdBar) // FIXME: @lf94 has a better way to verify execution completion, in a PR rn
await scene.waitForExecutionDone()
await test.step(`Verify we see the sketch`, async () => { await test.step(`Verify we see the sketch`, async () => {
await scene.expectPixelColor(sketchColor, testPoint, 10) await scene.expectPixelColor(sketchColor, testPoint, 10)

View File

@ -1,16 +1,15 @@
import { FILE_EXT } from '@src/lib/constants' import { test, expect } from './zoo-test'
import * as fs from 'fs'
import * as fsp from 'fs/promises' import * as fsp from 'fs/promises'
import { join } from 'path' import * as fs from 'fs'
import { import {
createProject, createProject,
executorInputPath, executorInputPath,
getUtils, getUtils,
orRunWhenFullSuiteEnabled, orRunWhenFullSuiteEnabled,
runningOnWindows, runningOnWindows,
} from '@e2e/playwright/test-utils' } from './test-utils'
import { expect, test } from '@e2e/playwright/zoo-test' import { join } from 'path'
import { FILE_EXT } from 'lib/constants'
test.describe('integrations tests', () => { test.describe('integrations tests', () => {
test( test(
@ -47,7 +46,6 @@ test.describe('integrations tests', () => {
await scene.connectionEstablished() await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
await clickObj() await clickObj()
await page.waitForTimeout(1000)
await scene.moveNoWhere() await scene.moveNoWhere()
await editor.expectState({ await editor.expectState({
activeLines: [ activeLines: [
@ -73,11 +71,11 @@ test.describe('integrations tests', () => {
}) })
await test.step('setup for next assertion', async () => { await test.step('setup for next assertion', async () => {
await toolbar.openFile('main.kcl') await toolbar.openFile('main.kcl')
await page.waitForTimeout(1000)
await scene.settled(cmdBar)
await clickObj() await clickObj()
await page.waitForTimeout(1000)
await scene.moveNoWhere() await scene.moveNoWhere()
await page.waitForTimeout(1000)
await editor.expectState({ await editor.expectState({
activeLines: [ activeLines: [
'|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]', '|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]',
@ -90,7 +88,7 @@ test.describe('integrations tests', () => {
await toolbar.expectFileTreeState(['main.kcl', fileName]) await toolbar.expectFileTreeState(['main.kcl', fileName])
}) })
await test.step('check sketch mode is exited when opening a different file', async () => { await test.step('check sketch mode is exited when opening a different file', async () => {
await toolbar.openFile(fileName) await toolbar.openFile(fileName, { wait: false })
// check we're out of sketch mode // check we're out of sketch mode
await expect(toolbar.exitSketchBtn).not.toBeVisible() await expect(toolbar.exitSketchBtn).not.toBeVisible()

View File

@ -1,5 +1,5 @@
import type { Locator, Page, Request, Route, TestInfo } from '@playwright/test' import type { Page, Locator, Route, Request } from '@playwright/test'
import { expect } from '@playwright/test' import { expect, TestInfo } from '@playwright/test'
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
@ -112,7 +112,10 @@ export class CmdBarFixture {
* and assumes we are past the `pickCommand` step. * and assumes we are past the `pickCommand` step.
*/ */
progressCmdBar = async (shouldFuzzProgressMethod = true) => { progressCmdBar = async (shouldFuzzProgressMethod = true) => {
await this.page.waitForTimeout(2000) // FIXME: Progressing the command bar is a race condition. We have an async useEffect that reports the final state via useCalculateKclExpression. If this does not run quickly enough, it will not "fail" the continue because you can press continue if the state is not ready. E2E tests do not know this.
// Wait 1250ms to assume the await executeAst of the KCL input field is finished
await this.page.waitForTimeout(1250)
if (shouldFuzzProgressMethod || Math.random() > 0.5) {
const arrowButton = this.page.getByRole('button', { const arrowButton = this.page.getByRole('button', {
name: 'arrow right Continue', name: 'arrow right Continue',
}) })
@ -123,6 +126,9 @@ export class CmdBarFixture {
.getByRole('button', { name: 'checkmark Submit command' }) .getByRole('button', { name: 'checkmark Submit command' })
.click() .click()
} }
} else {
await this.page.keyboard.press('Enter')
}
} }
// Added data-testid to the command bar buttons // Added data-testid to the command bar buttons

View File

@ -1,12 +1,11 @@
import type { Locator, Page } from '@playwright/test' import type { Page, Locator } from '@playwright/test'
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { import {
checkIfPaneIsOpen,
closePane, closePane,
checkIfPaneIsOpen,
openPane, openPane,
sansWhitespace, sansWhitespace,
} from '@e2e/playwright/test-utils' } from '../test-utils'
interface EditorState { interface EditorState {
activeLines: Array<string> activeLines: Array<string>
@ -82,13 +81,6 @@ export class EditorFixture {
expectEditor = { expectEditor = {
toContain: this._expectEditorToContain(), toContain: this._expectEditorToContain(),
not: { toContain: this._expectEditorToContain(true) }, not: { toContain: this._expectEditorToContain(true) },
toBe: async (code: string) => {
const currentCode = await this.getCurrentCode()
return expect(currentCode).toBe(code)
},
}
getCurrentCode = async () => {
return await this.codeContent.innerText()
} }
snapshot = async (options?: { timeout?: number; name?: string }) => { snapshot = async (options?: { timeout?: number; name?: string }) => {
const wasPaneOpen = await this.checkIfPaneIsOpen() const wasPaneOpen = await this.checkIfPaneIsOpen()

View File

@ -1,28 +1,28 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import type { import type {
BrowserContext, BrowserContext,
ElectronApplication, ElectronApplication,
Page,
TestInfo, TestInfo,
Page,
} from '@playwright/test' } from '@playwright/test'
import { _electron as electron } from '@playwright/test' import { _electron as electron } from '@playwright/test'
import { SETTINGS_FILE_NAME } from '@src/lib/constants' import * as TOML from '@iarna/toml'
import type { DeepPartial } from '@src/lib/types' import { TEST_SETTINGS } from '../storageStates'
import { SETTINGS_FILE_NAME } from 'lib/constants'
import { getUtils, setup } from '../test-utils'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import fs from 'node:fs' import fs from 'node:fs'
import path from 'path' import path from 'path'
import { CmdBarFixture } from './cmdBarFixture'
import type { Settings } from '@rust/kcl-lib/bindings/Settings' import { EditorFixture } from './editorFixture'
import { ToolbarFixture } from './toolbarFixture'
import { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture' import { SceneFixture } from './sceneFixture'
import { EditorFixture } from '@e2e/playwright/fixtures/editorFixture' import { HomePageFixture } from './homePageFixture'
import { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture' import { DeepPartial } from 'lib/types'
import { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture' import { Settings } from '@rust/kcl-lib/bindings/Settings'
import { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
import { TEST_SETTINGS } from '@e2e/playwright/storageStates'
import { getUtils, settingsToToml, setup } from '@e2e/playwright/test-utils'
export class AuthenticatedApp { export class AuthenticatedApp {
public readonly page: Page public readonly page: Page
@ -39,8 +39,7 @@ export class AuthenticatedApp {
} }
async initialise(code = '') { async initialise(code = '') {
const testDir = this.testInfo.outputPath('electron-test-projects-dir') await setup(this.context, this.page, this.testInfo)
await setup(this.context, this.page, testDir, this.testInfo)
const u = await getUtils(this.page) const u = await getUtils(this.page)
await this.page.addInitScript(async (code) => { await this.page.addInitScript(async (code) => {
@ -103,11 +102,11 @@ export class ElectronZoo {
return resolve(undefined) return resolve(undefined)
} }
if (Date.now() - timeA > 3000) { if (Date.now() - timeA > 10000) {
return resolve(undefined) return resolve(undefined)
} }
setTimeout(checkDisconnected, 1) setTimeout(checkDisconnected, 0)
} }
checkDisconnected() checkDisconnected()
}) })
@ -125,13 +124,14 @@ export class ElectronZoo {
// We need to expose this in order for some tests that require folder // We need to expose this in order for some tests that require folder
// creation and some code below. // creation and some code below.
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this const that = this
const options = { const options = {
timeout: 120000,
args: ['.', '--no-sandbox'], args: ['.', '--no-sandbox'],
env: { env: {
...process.env, ...process.env,
TEST_SETTINGS_FILE_KEY: this.projectDirName,
IS_PLAYWRIGHT: 'true', IS_PLAYWRIGHT: 'true',
}, },
...(process.env.ELECTRON_OVERRIDE_DIST_PATH ...(process.env.ELECTRON_OVERRIDE_DIST_PATH
@ -176,37 +176,11 @@ export class ElectronZoo {
this.context = this.electron.context() this.context = this.electron.context()
await this.context.tracing.start({ screenshots: true, snapshots: true }) await this.context.tracing.start({ screenshots: true, snapshots: true })
// We need to patch this because addInitScript will bind too late in our
// electron tests, never running. We need to call reload() after each call
// to guarantee it runs.
const oldContextAddInitScript = this.context.addInitScript
this.context.addInitScript = async function (a, b) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldContextAddInitScript.apply(this, [a, b])
await that.page.reload()
}
const oldPageAddInitScript = this.page.addInitScript
this.page.addInitScript = async function (a: any, b: any) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldPageAddInitScript.apply(this, [a, b])
await that.page.reload()
}
} }
await this.context.tracing.startChunk() await this.context.tracing.startChunk()
// THIS IS ABSOLUTELY NECESSARY TO CHANGE THE PROJECT DIRECTORY BETWEEN await setup(this.context, this.page, testInfo)
// TESTS BECAUSE OF THE ELECTRON INSTANCE REUSE.
await this.electron?.evaluate(({ app }, projectDirName) => {
// @ts-ignore can't declaration merge see main.ts
app.testProperty['TEST_SETTINGS_FILE_KEY'] = projectDirName
}, this.projectDirName)
await setup(this.context, this.page, this.projectDirName, testInfo)
await this.cleanProjectDir() await this.cleanProjectDir()
@ -244,6 +218,26 @@ export class ElectronZoo {
})) }))
} }
// We need to patch this because addInitScript will bind too late in our
// electron tests, never running. We need to call reload() after each call
// to guarantee it runs.
const oldContextAddInitScript = this.context.addInitScript
this.context.addInitScript = async function (a, b) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldContextAddInitScript.apply(this, [a, b])
await that.page.reload()
}
// No idea why we mix and match page and context's addInitScript but we do
const oldPageAddInitScript = this.page.addInitScript
this.page.addInitScript = async function (a: any, b: any) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldPageAddInitScript.apply(this, [a, b])
await that.page.reload()
}
if (!this.firstUrl) { if (!this.firstUrl) {
await this.page.getByText('Your Projects').count() await this.page.getByText('Your Projects').count()
this.firstUrl = this.page.url() this.firstUrl = this.page.url()
@ -256,6 +250,11 @@ export class ElectronZoo {
// return app.reuseWindowForTest(); // return app.reuseWindowForTest();
// }); // });
await this.electron?.evaluate(({ app }, projectDirName) => {
// @ts-ignore can't declaration merge see main.ts
app.testProperty['TEST_SETTINGS_FILE_KEY'] = projectDirName
}, this.projectDirName)
// Always start at the root view // Always start at the root view
await this.page.goto(this.firstUrl) await this.page.goto(this.firstUrl)
@ -279,39 +278,34 @@ export class ElectronZoo {
// Not a problem if it already exists. // Not a problem if it already exists.
} }
const tempSettingsFilePath = path.resolve( const tempSettingsFilePath = path.join(
this.projectDirName, this.projectDirName,
'..',
SETTINGS_FILE_NAME SETTINGS_FILE_NAME
) )
let settingsOverridesToml = '' let settingsOverridesToml = ''
if (appSettings) { if (appSettings) {
settingsOverridesToml = settingsToToml({ settingsOverridesToml = TOML.stringify({
// @ts-expect-error
settings: { settings: {
...TEST_SETTINGS, ...TEST_SETTINGS,
...appSettings, ...appSettings,
app: { app: {
...TEST_SETTINGS.app, ...TEST_SETTINGS.app,
project_directory: this.projectDirName,
...appSettings.app, ...appSettings.app,
}, },
project: {
...TEST_SETTINGS.project,
directory: this.projectDirName,
},
}, },
}) })
} else { } else {
settingsOverridesToml = settingsToToml({ settingsOverridesToml = TOML.stringify({
// @ts-expect-error
settings: { settings: {
...TEST_SETTINGS, ...TEST_SETTINGS,
app: { app: {
...TEST_SETTINGS.app, ...TEST_SETTINGS.app,
}, project_directory: this.projectDirName,
project: {
...TEST_SETTINGS.project,
directory: this.projectDirName,
}, },
}, },
}) })

View File

@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test' import type { Page, Locator } from '@playwright/test'
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
interface ProjectCardState { interface ProjectCardState {
@ -96,7 +96,7 @@ export class HomePageFixture {
await expect(this.projectSection).not.toHaveText('Loading your Projects...') await expect(this.projectSection).not.toHaveText('Loading your Projects...')
} }
createAndGoToProject = async (projectTitle = 'untitled') => { createAndGoToProject = async (projectTitle = 'project-$nnn') => {
await this.projectsLoaded() await this.projectsLoaded()
await this.projectButtonNew.click() await this.projectButtonNew.click()
await this.projectTextName.click() await this.projectTextName.click()

View File

@ -1,17 +1,15 @@
import type { Locator, Page } from '@playwright/test' import type { Page, Locator } from '@playwright/test'
import { isArray, uuidv4 } from '@src/lib/utils' import { expect } from '../zoo-test'
import { isArray, uuidv4 } from 'lib/utils'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture' import { CmdBarFixture } from './cmdBarFixture'
import { import {
closeDebugPanel, closeDebugPanel,
doAndWaitForImageDiff, doAndWaitForImageDiff,
getPixelRGBs, getPixelRGBs,
getUtils,
openAndClearDebugPanel, openAndClearDebugPanel,
sendCustomCmd, sendCustomCmd,
} from '@e2e/playwright/test-utils' getUtils,
import { expect } from '@e2e/playwright/zoo-test' } from '../test-utils'
type MouseParams = { type MouseParams = {
pixelDiff?: number pixelDiff?: number
@ -43,13 +41,21 @@ type DragFromHandler = (
export class SceneFixture { export class SceneFixture {
public page: Page public page: Page
public streamWrapper!: Locator public streamWrapper!: Locator
public loadingIndicator!: Locator
public networkToggleConnected!: Locator public networkToggleConnected!: Locator
public startEditSketchBtn!: Locator public startEditSketchBtn!: Locator
get exeIndicator() {
return this.page
.getByTestId('model-state-indicator-execution-done')
.or(this.page.getByTestId('model-state-indicator-receive-reliable'))
}
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.streamWrapper = page.getByTestId('stream') this.streamWrapper = page.getByTestId('stream')
this.networkToggleConnected = page.getByTestId('network-toggle-ok') this.networkToggleConnected = page.getByTestId('network-toggle-ok')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
this.startEditSketchBtn = page this.startEditSketchBtn = page
.getByRole('button', { name: 'Start Sketch' }) .getByRole('button', { name: 'Start Sketch' })
.or(page.getByRole('button', { name: 'Edit Sketch' })) .or(page.getByRole('button', { name: 'Edit Sketch' }))
@ -223,6 +229,10 @@ export class SceneFixture {
} }
} }
waitForExecutionDone = async () => {
await expect(this.exeIndicator).toBeVisible({ timeout: 30000 })
}
connectionEstablished = async () => { connectionEstablished = async () => {
const timeout = 30000 const timeout = 30000
await expect(this.networkToggleConnected).toBeVisible({ timeout }) await expect(this.networkToggleConnected).toBeVisible({ timeout })
@ -231,9 +241,6 @@ export class SceneFixture {
settled = async (cmdBar: CmdBarFixture) => { settled = async (cmdBar: CmdBarFixture) => {
const u = await getUtils(this.page) const u = await getUtils(this.page)
await expect(this.startEditSketchBtn).not.toBeDisabled()
await expect(this.startEditSketchBtn).toBeVisible()
await cmdBar.openCmdBar() await cmdBar.openCmdBar()
await cmdBar.chooseCommand('Settings · app · show debug panel') await cmdBar.chooseCommand('Settings · app · show debug panel')
await cmdBar.selectOption({ name: 'on' }).click() await cmdBar.selectOption({ name: 'on' }).click()
@ -241,6 +248,10 @@ export class SceneFixture {
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
await this.waitForExecutionDone()
await expect(this.startEditSketchBtn).not.toBeDisabled()
await expect(this.startEditSketchBtn).toBeVisible()
} }
expectPixelColor = async ( expectPixelColor = async (
@ -299,9 +310,7 @@ export async function expectPixelColor(
.toBeTruthy() .toBeTruthy()
.catch((cause) => { .catch((cause) => {
throw new Error( throw new Error(
`ExpectPixelColor: point ${JSON.stringify( `ExpectPixelColor: expecting ${colour} got ${finalValue}`,
coords
)} was expecting ${colour} but got ${finalValue}`,
{ cause } { cause }
) )
}) })

View File

@ -1,18 +1,14 @@
import { type Locator, type Page, test } from '@playwright/test' import { type Page, type Locator, test } from '@playwright/test'
import type { SidebarType } from '@src/components/ModelingSidebar/ModelingPanes' import { expect } from '../zoo-test'
import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants'
import type { ToolbarModeName } from '@src/lib/toolbar'
import { import {
checkIfPaneIsOpen, checkIfPaneIsOpen,
closePane, closePane,
doAndWaitForImageDiff, doAndWaitForImageDiff,
openPane, openPane,
} from '@e2e/playwright/test-utils' } from '../test-utils'
import { expect } from '@e2e/playwright/zoo-test' import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
import { type baseUnitLabels } from '@src/lib/settings/settingsTypes' import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
import { ToolbarModeName } from 'lib/toolbar'
type LengthUnitLabel = (typeof baseUnitLabels)[keyof typeof baseUnitLabels]
export class ToolbarFixture { export class ToolbarFixture {
public page: Page public page: Page
@ -44,7 +40,6 @@ export class ToolbarFixture {
featureTreePane!: Locator featureTreePane!: Locator
gizmo!: Locator gizmo!: Locator
gizmoDisabled!: Locator gizmoDisabled!: Locator
insertButton!: Locator
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
@ -79,14 +74,18 @@ export class ToolbarFixture {
// element or two different elements can represent these states. // element or two different elements can represent these states.
this.gizmo = page.getByTestId('gizmo') this.gizmo = page.getByTestId('gizmo')
this.gizmoDisabled = page.getByTestId('gizmo-disabled') this.gizmoDisabled = page.getByTestId('gizmo-disabled')
this.insertButton = page.getByTestId('insert-pane-button')
} }
get logoLink() { get logoLink() {
return this.page.getByTestId('app-logo') return this.page.getByTestId('app-logo')
} }
get exeIndicator() {
return this.page
.getByTestId('model-state-indicator-receive-reliable')
.or(this.page.getByTestId('model-state-indicator-execution-done'))
}
startSketchPlaneSelection = async () => startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500) doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
@ -162,10 +161,16 @@ export class ToolbarFixture {
} }
} }
/** /**
* Opens file by it's name * Opens file by it's name and waits for execution to finish
*/ */
openFile = async (fileName: string) => { openFile = async (
fileName: string,
{ wait }: { wait?: boolean } = { wait: true }
) => {
await this.filePane.getByText(fileName).click() await this.filePane.getByText(fileName).click()
if (wait) {
await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
}
} }
selectCenterRectangle = async () => { selectCenterRectangle = async () => {
await this.page await this.page
@ -176,14 +181,6 @@ export class ToolbarFixture {
).toBeVisible() ).toBeVisible()
await this.page.getByTestId('dropdown-center-rectangle').click() await this.page.getByTestId('dropdown-center-rectangle').click()
} }
selectBoolean = async (operation: 'union' | 'subtract' | 'intersect') => {
await this.page
.getByRole('button', { name: 'caret down Union: open menu' })
.click()
const operationTestId = `dropdown-boolean-${operation}`
await expect(this.page.getByTestId(operationTestId)).toBeVisible()
await this.page.getByTestId(operationTestId).click()
}
selectCircleThreePoint = async () => { selectCircleThreePoint = async () => {
await this.page await this.page
@ -230,12 +227,6 @@ export class ToolbarFixture {
async checkIfFeatureTreePaneIsOpen() { async checkIfFeatureTreePaneIsOpen() {
return this.checkIfPaneIsOpen(this.featureTreeId) return this.checkIfPaneIsOpen(this.featureTreeId)
} }
async selectUnit(unit: LengthUnitLabel) {
await this.page.getByTestId('units-menu').click()
const optionLocator = this.page.getByRole('button', { name: unit })
await expect(optionLocator).toBeVisible()
await optionLocator.click()
}
/** /**
* Get a specific operation button from the Feature Tree pane. * Get a specific operation button from the Feature Tree pane.

View File

@ -1,121 +0,0 @@
import { expect, test } from '@e2e/playwright/zoo-test'
import * as fsp from 'fs/promises'
import path from 'path'
test.describe('Import UI tests', () => {
test('shows toast when trying to sketch on imported face', async ({
context,
page,
homePage,
toolbar,
scene,
editor,
cmdBar,
}) => {
await context.folderSetupFn(async (dir) => {
const projectDir = path.join(dir, 'import-test')
await fsp.mkdir(projectDir, { recursive: true })
// Create the imported file
await fsp.writeFile(
path.join(projectDir, 'toBeImported.kcl'),
`sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([281.54, 305.81], sketch001)
|> angledLine([0, 123.43], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
85.99
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude(profile001, length = 100)`
)
// Create the main file that imports
await fsp.writeFile(
path.join(projectDir, 'main.kcl'),
`import "toBeImported.kcl" as importedCube
importedCube
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([-134.53, -56.17], sketch001)
|> angledLine([0, 79.05], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
76.28
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
|> close()
extrude001 = extrude(profile001, length = 100)
sketch003 = startSketchOn(extrude001, seg02)
sketch002 = startSketchOn(extrude001, seg01)`
)
})
await homePage.openProject('import-test')
await scene.settled(cmdBar)
await scene.moveCameraTo(
{
x: -114,
y: -897,
z: 475,
},
{
x: -114,
y: -51,
z: 83,
}
)
const [_, hoverOverNonImport] = scene.makeMouseHelpers(611, 364)
const [importedFaceClick, hoverOverImported] = scene.makeMouseHelpers(
940,
150
)
await test.step('check code highlight works for code define in the file being edited', async () => {
await hoverOverNonImport()
await editor.expectState({
highlightedCode: 'startProfileAt([-134.53,-56.17],sketch001)',
diagnostics: [],
activeLines: ['import"toBeImported.kcl"asimportedCube'],
})
})
await test.step('check code does nothing when geometry is defined in an import', async () => {
await hoverOverImported()
await editor.expectState({
highlightedCode: '',
diagnostics: [],
activeLines: ['import"toBeImported.kcl"asimportedCube'],
})
})
await test.step('check the user is warned when sketching on a imported face', async () => {
// Start sketch mode
await toolbar.startSketchPlaneSelection()
// Click on a face from the imported model
// await new Promise(() => {})
await importedFaceClick()
// Verify toast appears with correct content
await expect(page.getByText('This face is from an import')).toBeVisible()
await expect(
page.locator('.font-mono').getByText('toBeImported.kcl')
).toBeVisible()
await expect(
page.getByText('Please select this from the files pane to edit')
).toBeVisible()
})
})
})

View File

@ -258,6 +258,14 @@ export const isErrorWhitelisted = (exception: Error) => {
foundInSpec: 'e2e/playwright/testing-settings.spec.ts', foundInSpec: 'e2e/playwright/testing-settings.spec.ts',
}, },
// TODO: fix this error in the code // TODO: fix this error in the code
{
name: 'TypeError',
message: "Cannot read properties of undefined (reading 'length')",
stack: '',
project: 'Google Chrome',
foundInSpec: '', // many tests are impacted by this error
},
// TODO: fix this error in the code
{ {
name: 'ReferenceError', name: 'ReferenceError',
message: '_testUtils is not defined', message: '_testUtils is not defined',
@ -265,6 +273,7 @@ export const isErrorWhitelisted = (exception: Error) => {
project: 'Google Chrome', project: 'Google Chrome',
foundInSpec: 'e2e/playwright/snapshot-tests.spec.ts', foundInSpec: 'e2e/playwright/snapshot-tests.spec.ts',
}, },
// TODO: fix this error in the code
{ {
name: 'TypeError', name: 'TypeError',
message: 'Failed to fetch', message: 'Failed to fetch',
@ -272,13 +281,21 @@ export const isErrorWhitelisted = (exception: Error) => {
project: 'Google Chrome', project: 'Google Chrome',
foundInSpec: 'e2e/playwright/snapshot-tests.spec.ts', foundInSpec: 'e2e/playwright/snapshot-tests.spec.ts',
}, },
// TODO: fix this error in the code
{ {
name: 'Error', name: 'ReferenceError',
message: 'The "path" argument must be of type string. Received undefined', message: 'originalCode is not defined',
stack: stack: '',
'Error: The "path" argument must be of type string. Received undefined',
project: 'Google Chrome', project: 'Google Chrome',
foundInSpec: '', // many tests are impacted by this error foundInSpec: 'e2e/playwright/onboarding-tests.spec.ts',
},
// TODO: fix this error in the code
{
name: 'ReferenceError',
message: 'createNewVariableCheckbox is not defined',
stack: '',
project: 'Google Chrome',
foundInSpec: 'e2e/playwright/testing-constraints.spec.ts',
}, },
] ]

View File

@ -1,7 +0,0 @@
export const throwError = (message: string): never => {
throw new Error(message)
}
export const throwTronAppMissing = () => {
throwError('tronApp is missing')
}

View File

@ -1,18 +1,17 @@
import fsp from 'fs/promises' import { test, expect } from './zoo-test'
import { executorInputPath } from './test-utils'
import { join } from 'path' import { join } from 'path'
import fsp from 'fs/promises'
import { executorInputPath } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test( test(
'When machine-api server not found butt is disabled and shows the reason', 'When machine-api server not found butt is disabled and shows the reason',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page, scene, cmdBar }, testInfo) => { async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const bracketDir = join(dir, 'bracket') const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('cylinder-inches.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl') join(bracketDir, 'main.kcl')
) )
}) })
@ -23,7 +22,10 @@ test(
await page.getByText('bracket').click() await page.getByText('bracket').click()
await scene.settled(cmdBar) await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
const notFoundText = 'Machine API server was not discovered' const notFoundText = 'Machine API server was not discovered'
await expect(page.getByText(notFoundText).first()).not.toBeVisible() await expect(page.getByText(notFoundText).first()).not.toBeVisible()
@ -44,12 +46,12 @@ test(
test( test(
'When machine-api server not found home screen & project status shows the reason', 'When machine-api server not found home screen & project status shows the reason',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page, scene, cmdBar }, testInfo) => { async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const bracketDir = join(dir, 'bracket') const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('cylinder-inches.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl') join(bracketDir, 'main.kcl')
) )
}) })
@ -68,7 +70,10 @@ test(
await page.getByText('bracket').click() await page.getByText('bracket').click()
await scene.settled(cmdBar) await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(page.getByText(notFoundText).nth(1)).not.toBeVisible() await expect(page.getByText(notFoundText).nth(1)).not.toBeVisible()

View File

@ -1,15 +1,13 @@
import { PROJECT_SETTINGS_FILE_NAME } from '@src/lib/constants' import { test, expect } from './zoo-test'
import { PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
import * as fsp from 'fs/promises' import * as fsp from 'fs/promises'
import { join } from 'path' import { join } from 'path'
import type { NamedView } from '@rust/kcl-lib/bindings/NamedView'
import { import {
createProject, createProject,
perProjectsettingsToToml,
tomlToPerProjectSettings, tomlToPerProjectSettings,
} from '@e2e/playwright/test-utils' perProjectsettingsToToml,
import { expect, test } from '@e2e/playwright/zoo-test' } from './test-utils'
import { NamedView } from '@rust/kcl-lib/bindings/NamedView'
// Helper function to determine if the file path on disk exists // Helper function to determine if the file path on disk exists
// Specifically this is used to check if project.toml exists on disk // Specifically this is used to check if project.toml exists on disk
@ -88,7 +86,7 @@ test.describe('Named view tests', () => {
// Create and load project // Create and load project
await createProject({ name: projectName, page }) await createProject({ name: projectName, page })
await scene.settled(cmdBar) await scene.waitForExecutionDone()
// Create named view // Create named view
const projectDirName = testInfo.outputPath('electron-test-projects-dir') const projectDirName = testInfo.outputPath('electron-test-projects-dir')
@ -110,17 +108,14 @@ test.describe('Named view tests', () => {
expect(exists).toBe(true) expect(exists).toBe(true)
}).toPass() }).toPass()
await expect(async () => {
// Read project.toml into memory // Read project.toml into memory
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8') let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break // Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
tomlString = tomlStringOverWriteNamedViewUuids(tomlString) tomlString = tomlStringOverWriteNamedViewUuids(tomlString)
// Write the entire tomlString to a snapshot. // Write the entire tomlString to a snapshot.
// There are many key/value pairs to check this is a safer match. // There are many key/value pairs to check this is a safer match.
expect(tomlString).toMatchSnapshot('verify-named-view-gets-created') expect(tomlString).toMatchSnapshot('verify-named-view-gets-created')
}).toPass()
}) })
test('Verify named view gets deleted', async ({ test('Verify named view gets deleted', async ({
cmdBar, cmdBar,
@ -133,7 +128,7 @@ test.describe('Named view tests', () => {
// Create project and go into the project // Create project and go into the project
await createProject({ name: projectName, page }) await createProject({ name: projectName, page })
await scene.settled(cmdBar) await scene.waitForExecutionDone()
// Create a new named view // Create a new named view
await cmdBar.openCmdBar() await cmdBar.openCmdBar()
@ -155,7 +150,6 @@ test.describe('Named view tests', () => {
expect(exists).toBe(true) expect(exists).toBe(true)
}).toPass() }).toPass()
await expect(async () => {
// Read project.toml into memory // Read project.toml into memory
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8') let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break // Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
@ -164,7 +158,6 @@ test.describe('Named view tests', () => {
// Write the entire tomlString to a snapshot. // Write the entire tomlString to a snapshot.
// There are many key/value pairs to check this is a safer match. // There are many key/value pairs to check this is a safer match.
expect(tomlString).toMatchSnapshot('verify-named-view-gets-created') expect(tomlString).toMatchSnapshot('verify-named-view-gets-created')
}).toPass()
// Delete a named view // Delete a named view
await cmdBar.openCmdBar() await cmdBar.openCmdBar()
@ -172,16 +165,14 @@ test.describe('Named view tests', () => {
cmdBar.selectOption({ name: myNamedView2 }) cmdBar.selectOption({ name: myNamedView2 })
await cmdBar.progressCmdBar(false) await cmdBar.progressCmdBar(false)
await expect(async () => {
// Read project.toml into memory again since we deleted a named view // Read project.toml into memory again since we deleted a named view
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8') tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break // Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
tomlString = tomlStringOverWriteNamedViewUuids(tomlString) tomlString = tomlStringOverWriteNamedViewUuids(tomlString)
// // Write the entire tomlString to a snapshot. // // Write the entire tomlString to a snapshot.
// // There are many key/value pairs to check this is a safer match. // // There are many key/value pairs to check this is a safer match.
expect(tomlString).toMatchSnapshot('verify-named-view-gets-deleted') expect(tomlString).toMatchSnapshot('verify-named-view-gets-deleted')
}).toPass()
}) })
test('Verify named view gets loaded', async ({ test('Verify named view gets loaded', async ({
cmdBar, cmdBar,
@ -193,7 +184,7 @@ test.describe('Named view tests', () => {
// Create project and go into the project // Create project and go into the project
await createProject({ name: projectName, page }) await createProject({ name: projectName, page })
await scene.settled(cmdBar) await scene.waitForExecutionDone()
// Create a new named view // Create a new named view
await cmdBar.openCmdBar() await cmdBar.openCmdBar()
@ -215,7 +206,6 @@ test.describe('Named view tests', () => {
expect(exists).toBe(true) expect(exists).toBe(true)
}).toPass() }).toPass()
await expect(async () => {
// Read project.toml into memory // Read project.toml into memory
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8') let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break // Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
@ -224,7 +214,6 @@ test.describe('Named view tests', () => {
// Write the entire tomlString to a snapshot. // Write the entire tomlString to a snapshot.
// There are many key/value pairs to check this is a safer match. // There are many key/value pairs to check this is a safer match.
expect(tomlString).toMatchSnapshot('verify-named-view-gets-created') expect(tomlString).toMatchSnapshot('verify-named-view-gets-created')
}).toPass()
// Create a load a named view // Create a load a named view
await cmdBar.openCmdBar() await cmdBar.openCmdBar()
@ -248,7 +237,7 @@ test.describe('Named view tests', () => {
// Create and load project // Create and load project
await createProject({ name: projectName, page }) await createProject({ name: projectName, page })
await scene.settled(cmdBar) await scene.waitForExecutionDone()
// Create named view // Create named view
const projectDirName = testInfo.outputPath('electron-test-projects-dir') const projectDirName = testInfo.outputPath('electron-test-projects-dir')
@ -291,7 +280,6 @@ test.describe('Named view tests', () => {
expect(exists).toBe(true) expect(exists).toBe(true)
}).toPass() }).toPass()
await expect(async () => {
// Read project.toml into memory // Read project.toml into memory
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8') let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break // Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
@ -300,6 +288,5 @@ test.describe('Named view tests', () => {
// Write the entire tomlString to a snapshot. // Write the entire tomlString to a snapshot.
// There are many key/value pairs to check this is a safer match. // There are many key/value pairs to check this is a safer match.
expect(tomlString).toMatchSnapshot('verify-two-named-view-gets-created') expect(tomlString).toMatchSnapshot('verify-two-named-view-gets-created')
}).toPass()
}) })
}) })

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,8 @@
// application, check it can make it to the project pane, and nothing more. // application, check it can make it to the project pane, and nothing more.
// It also tests our test wrappers are working. // It also tests our test wrappers are working.
// Additionally this serves as a nice minimal example. // Additionally this serves as a nice minimal example.
import { expect, test } from '@e2e/playwright/zoo-test'
import { test, expect } from './zoo-test'
test.describe('Open the application', () => { test.describe('Open the application', () => {
test('see the project view', async ({ page, context }) => { test('see the project view', async ({ page, context }) => {

View File

@ -1,23 +1,22 @@
import { bracket } from '@src/lib/exampleKcl' import { test, expect } from './zoo-test'
import { onboardingPaths } from '@src/routes/Onboarding/paths'
import fsp from 'fs/promises'
import { join } from 'path' import { join } from 'path'
import fsp from 'fs/promises'
import { expectPixelColor } from '@e2e/playwright/fixtures/sceneFixture' import {
getUtils,
executorInputPath,
createProject,
settingsToToml,
orRunWhenFullSuiteEnabled,
} from './test-utils'
import { bracket } from 'lib/exampleKcl'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { import {
TEST_SETTINGS_KEY, TEST_SETTINGS_KEY,
TEST_SETTINGS_ONBOARDING_EXPORT,
TEST_SETTINGS_ONBOARDING_START, TEST_SETTINGS_ONBOARDING_START,
TEST_SETTINGS_ONBOARDING_EXPORT,
TEST_SETTINGS_ONBOARDING_USER_MENU, TEST_SETTINGS_ONBOARDING_USER_MENU,
} from '@e2e/playwright/storageStates' } from './storageStates'
import { import { expectPixelColor } from './fixtures/sceneFixture'
createProject,
executorInputPath,
getUtils,
orRunWhenFullSuiteEnabled,
settingsToToml,
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
// Because our default test settings have the onboardingStatus set to 'dismissed', // Because our default test settings have the onboardingStatus set to 'dismissed',
// we must set it to empty for the tests where we want to see the onboarding immediately. // we must set it to empty for the tests where we want to see the onboarding immediately.
@ -42,10 +41,10 @@ test.describe('Onboarding tests', () => {
await homePage.goToModelingScene() await homePage.goToModelingScene()
// Test that the onboarding pane loaded // Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Design Studio! This')).toBeVisible() await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
// Test that the onboarding pane loaded // Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Design Studio! This')).toBeVisible() await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
// *and* that the code is shown in the editor // *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket') await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
@ -86,7 +85,7 @@ test.describe('Onboarding tests', () => {
await test.step(`Ensure we see the onboarding stuff`, async () => { await test.step(`Ensure we see the onboarding stuff`, async () => {
// Test that the onboarding pane loaded // Test that the onboarding pane loaded
await expect( await expect(
page.getByText('Welcome to Design Studio! This') page.getByText('Welcome to Modeling App! This')
).toBeVisible() ).toBeVisible()
// *and* that the code is shown in the editor // *and* that the code is shown in the editor
@ -147,7 +146,7 @@ test.describe('Onboarding tests', () => {
await nextButton.click() await nextButton.click()
// Ensure we see the introduction and that the code has been reset // Ensure we see the introduction and that the code has been reset
await expect(page.getByText('Welcome to Design Studio!')).toBeVisible() await expect(page.getByText('Welcome to Modeling App!')).toBeVisible()
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket') await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// There used to be old code here that checked if we stored the reset // There used to be old code here that checked if we stored the reset
@ -188,7 +187,7 @@ test.describe('Onboarding tests', () => {
await homePage.goToModelingScene() await homePage.goToModelingScene()
// Test that the onboarding pane loaded // Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Design Studio! This')).toBeVisible() await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
const nextButton = page.getByTestId('onboarding-next') const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev') const prevButton = page.getByTestId('onboarding-prev')
@ -231,9 +230,9 @@ test.describe('Onboarding tests', () => {
// Override beforeEach test setup // Override beforeEach test setup
await context.addInitScript( await context.addInitScript(
async ({ settingsKey, settings, code }) => { async ({ settingsKey, settings }) => {
// Give some initial code, so we can test that it's cleared // Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', originalCode)
localStorage.setItem(settingsKey, settings) localStorage.setItem(settingsKey, settings)
}, },
{ {
@ -241,7 +240,6 @@ test.describe('Onboarding tests', () => {
settings: settingsToToml({ settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_EXPORT, settings: TEST_SETTINGS_ONBOARDING_EXPORT,
}), }),
code: originalCode,
} }
) )
@ -494,7 +492,7 @@ test('Restarting onboarding on desktop takes one attempt', async ({
const tutorialProjectIndicator = page const tutorialProjectIndicator = page
.getByTestId('project-sidebar-toggle') .getByTestId('project-sidebar-toggle')
.filter({ hasText: 'Tutorial Project 00' }) .filter({ hasText: 'Tutorial Project 00' })
const tutorialModalText = page.getByText('Welcome to Design Studio!') const tutorialModalText = page.getByText('Welcome to Modeling App!')
const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' }) const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' })
const userMenuButton = page.getByTestId('user-sidebar-toggle') const userMenuButton = page.getByTestId('user-sidebar-toggle')
const userMenuSettingsButton = page.getByRole('button', { const userMenuSettingsButton = page.getByRole('button', {

View File

@ -1,4 +1,4 @@
import { expect, test } from '@playwright/test' import { test, expect } from '@playwright/test'
/** @deprecated, import from ./fixtureSetup.ts instead */ /** @deprecated, import from ./fixtureSetup.ts instead */
export const _test = test export const _test = test

View File

@ -1,115 +0,0 @@
import * as fsp from 'fs/promises'
import path from 'path'
import { executorInputPath } from '@e2e/playwright/test-utils'
import { test } from '@e2e/playwright/zoo-test'
// test file is for testing point an click code gen functionality that's assemblies related
test.describe('Point-and-click assemblies tests', () => {
test(
`Insert kcl part into assembly as whole module import`,
{ tag: ['@electron'] },
async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
tronApp,
}) => {
if (!tronApp) {
fail()
}
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const initialColor: [number, number, number] = [50, 50, 50]
const partColor: [number, number, number] = [150, 150, 150]
const tolerance = 50
await test.step('Setup parts and expect empty assembly scene', async () => {
const projectName = 'assembly'
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, projectName)
await fsp.mkdir(bracketDir, { recursive: true })
await Promise.all([
fsp.copyFile(
executorInputPath('cylinder-inches.kcl'),
path.join(bracketDir, 'cylinder.kcl')
),
fsp.copyFile(
executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
path.join(bracketDir, 'bracket.kcl')
),
fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''),
])
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.openProject(projectName)
await scene.settled(cmdBar)
await scene.expectPixelColor(initialColor, testPoint, tolerance)
})
await test.step('Insert first part into the assembly', async () => {
await toolbar.insertButton.click()
await cmdBar.selectOption({ name: 'cylinder.kcl' }).click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'localName',
currentArgValue: '',
headerArguments: { Path: 'cylinder.kcl', LocalName: '' },
highlightedHeaderArg: 'localName',
commandName: 'Insert',
})
await page.keyboard.insertText('cylinder')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Path: 'cylinder.kcl', LocalName: 'cylinder' },
commandName: 'Insert',
})
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain(
`
import "cylinder.kcl" as cylinder
cylinder
`,
{ shouldNormalise: true }
)
await scene.expectPixelColor(partColor, testPoint, tolerance)
})
await test.step('Insert second part into the assembly', async () => {
await toolbar.insertButton.click()
await cmdBar.selectOption({ name: 'bracket.kcl' }).click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'localName',
currentArgValue: '',
headerArguments: { Path: 'bracket.kcl', LocalName: '' },
highlightedHeaderArg: 'localName',
commandName: 'Insert',
})
await page.keyboard.insertText('bracket')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Path: 'bracket.kcl', LocalName: 'bracket' },
commandName: 'Insert',
})
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain(
`
import "cylinder.kcl" as cylinder
import "bracket.kcl" as bracket
cylinder
bracket
`,
{ shouldNormalise: true }
)
})
}
)
})

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,19 @@
import { DEFAULT_PROJECT_KCL_FILE } from '@src/lib/constants' import { test, expect } from './zoo-test'
import fs from 'fs'
import fsp from 'fs/promises'
import path from 'path'
import type { Paths } from '@e2e/playwright/test-utils'
import { import {
createProject,
doExport, doExport,
executorInputPath, executorInputPath,
getPlaywrightDownloadDir,
getUtils, getUtils,
isOutOfViewInScrollContainer, isOutOfViewInScrollContainer,
Paths,
createProject,
getPlaywrightDownloadDir,
orRunWhenFullSuiteEnabled, orRunWhenFullSuiteEnabled,
runningOnWindows, runningOnWindows,
} from '@e2e/playwright/test-utils' } from './test-utils'
import { expect, test } from '@e2e/playwright/zoo-test' import fsp from 'fs/promises'
import fs from 'fs'
import path from 'path'
import { DEFAULT_PROJECT_KCL_FILE } from 'lib/constants'
test( test(
'projects reload if a new one is created, deleted, or renamed externally', 'projects reload if a new one is created, deleted, or renamed externally',
@ -83,23 +82,29 @@ test(
test( test(
'click help/keybindings from project page', 'click help/keybindings from project page',
{ tag: '@electron' }, { tag: '@electron' },
async ({ scene, cmdBar, context, page }, testInfo) => { async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('cylinder-inches.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
path.join(bracketDir, 'main.kcl') path.join(bracketDir, 'main.kcl')
) )
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log)
// expect to see the text bracket // expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click() await page.getByText('bracket').click()
await scene.settled(cmdBar) await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
// click ? button // click ? button
await page.getByTestId('help-button').click() await page.getByTestId('help-button').click()
@ -114,12 +119,12 @@ test(
test( test(
'open a file in a project works and renders, open another file in different project with errors, it should clear the scene', 'open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
{ tag: '@electron' }, { tag: '@electron' },
async ({ scene, cmdBar, context, page, editor }, testInfo) => { async ({ context, page, editor }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('cylinder-inches.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
path.join(bracketDir, 'main.kcl') path.join(bracketDir, 'main.kcl')
) )
const errorDir = path.join(dir, 'broken-code') const errorDir = path.join(dir, 'broken-code')
@ -143,7 +148,24 @@ test(
await page.getByText('bracket').click() await page.getByText('bracket').click()
await scene.settled(cmdBar) await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(20)
}) })
await test.step('Clicking the logo takes us back to the projects page / home', async () => { await test.step('Clicking the logo takes us back to the projects page / home', async () => {
@ -186,12 +208,12 @@ test(
test( test(
'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene', 'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
{ tag: '@electron' }, { tag: '@electron' },
async ({ scene, cmdBar, context, page }, testInfo) => { async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('cylinder-inches.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
path.join(bracketDir, 'main.kcl') path.join(bracketDir, 'main.kcl')
) )
const emptyDir = path.join(dir, 'empty') const emptyDir = path.join(dir, 'empty')
@ -212,7 +234,24 @@ test(
await page.getByText('bracket').click() await page.getByText('bracket').click()
await scene.settled(cmdBar) await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
}) })
await test.step('Clicking the logo takes us back to the projects page / home', async () => { await test.step('Clicking the logo takes us back to the projects page / home', async () => {
@ -251,7 +290,7 @@ test(
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('cylinder-inches.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
path.join(bracketDir, 'main.kcl') path.join(bracketDir, 'main.kcl')
) )
@ -280,7 +319,7 @@ test(
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [125, 125, 125]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(15) .toBeLessThan(15)
@ -312,7 +351,7 @@ test(
test( test(
'open a file in a project works and renders, open another file in the same project with errors, it should clear the scene', 'open a file in a project works and renders, open another file in the same project with errors, it should clear the scene',
{ tag: '@electron' }, { tag: '@electron' },
async ({ scene, cmdBar, context, page }, testInfo) => { async ({ context, page }, testInfo) => {
if (runningOnWindows()) { if (runningOnWindows()) {
test.fixme(orRunWhenFullSuiteEnabled()) test.fixme(orRunWhenFullSuiteEnabled())
} }
@ -320,7 +359,7 @@ test(
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('cylinder-inches.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
path.join(bracketDir, 'main.kcl') path.join(bracketDir, 'main.kcl')
) )
await fsp.copyFile( await fsp.copyFile(
@ -340,7 +379,10 @@ test(
await page.getByText('bracket').click() await page.getByText('bracket').click()
await scene.settled(cmdBar) await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
@ -351,7 +393,7 @@ test(
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [125, 125, 125]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(15) .toBeLessThan(15)
@ -400,10 +442,11 @@ test(
await expect(page.getByText('broken-code')).toBeVisible() await expect(page.getByText('broken-code')).toBeVisible()
await page.getByText('broken-code').click() await page.getByText('broken-code').click()
// Gotcha: You can not use scene.settled() since the KCL code is going to fail // Gotcha: You can not use scene.waitForExecutionDone() since the KCL code is going to fail
await expect( await expect(page.getByTestId('loading')).toBeAttached()
page.getByTestId('model-state-indicator-playing') await expect(page.getByTestId('loading')).not.toBeAttached({
).toBeAttached() timeout: 20_000,
})
// Gotcha: Scroll to the text content in code mirror because CodeMirror lazy loads DOM content // Gotcha: Scroll to the text content in code mirror because CodeMirror lazy loads DOM content
await editor.scrollToText( await editor.scrollToText(
@ -426,7 +469,7 @@ test.describe('Can export from electron app', () => {
test( test(
`Can export using ${method}`, `Can export using ${method}`,
{ tag: ['@electron', '@skipLocalEngine'] }, { tag: ['@electron', '@skipLocalEngine'] },
async ({ scene, cmdBar, context, page, tronApp }, testInfo) => { async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) { if (!tronApp) {
fail() fail()
} }
@ -438,7 +481,7 @@ test.describe('Can export from electron app', () => {
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('cylinder-inches.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
path.join(bracketDir, 'main.kcl') path.join(bracketDir, 'main.kcl')
) )
}) })
@ -456,7 +499,10 @@ test.describe('Can export from electron app', () => {
await page.getByText('bracket').click() await page.getByText('bracket').click()
await scene.settled(cmdBar) await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
@ -467,7 +513,7 @@ test.describe('Can export from electron app', () => {
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [125, 125, 125]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(15) .toBeLessThan(15)
@ -508,7 +554,7 @@ test.describe('Can export from electron app', () => {
}, },
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBeGreaterThan(50_000) .toBeGreaterThan(300_000)
// clean up exported file // clean up exported file
await fsp.rm(filepath) await fsp.rm(filepath)
@ -766,7 +812,7 @@ test.describe(`Project management commands`, () => {
test( test(
`Rename from project page`, `Rename from project page`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page, scene, cmdBar }, testInfo) => { async ({ context, page }, testInfo) => {
const projectName = `my_project_to_rename` const projectName = `my_project_to_rename`
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
@ -775,13 +821,14 @@ test.describe(`Project management commands`, () => {
`${dir}/${projectName}/main.kcl` `${dir}/${projectName}/main.kcl`
) )
}) })
const u = await getUtils(page)
// Constants and locators // Constants and locators
const projectHomeLink = page.getByTestId('project-link') const projectHomeLink = page.getByTestId('project-link')
const commandButton = page.getByRole('button', { name: 'Commands' }) const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'rename project' }) const commandOption = page.getByRole('option', { name: 'rename project' })
const projectNameOption = page.getByRole('option', { name: projectName }) const projectNameOption = page.getByRole('option', { name: projectName })
const projectRenamedName = `untitled` const projectRenamedName = `project-000`
// const projectMenuButton = page.getByTestId('project-sidebar-toggle') // const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const commandContinueButton = page.getByRole('button', { const commandContinueButton = page.getByRole('button', {
name: 'Continue', name: 'Continue',
@ -796,7 +843,7 @@ test.describe(`Project management commands`, () => {
page.on('console', console.log) page.on('console', console.log)
await projectHomeLink.click() await projectHomeLink.click()
await scene.settled(cmdBar) await u.waitForPageLoad()
}) })
await test.step(`Run rename command via command palette`, async () => { await test.step(`Run rename command via command palette`, async () => {
@ -835,6 +882,7 @@ test.describe(`Project management commands`, () => {
`${dir}/${projectName}/main.kcl` `${dir}/${projectName}/main.kcl`
) )
}) })
const u = await getUtils(page)
// Constants and locators // Constants and locators
const projectHomeLink = page.getByTestId('project-link') const projectHomeLink = page.getByTestId('project-link')
@ -852,9 +900,9 @@ test.describe(`Project management commands`, () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await page.waitForTimeout(3000)
await projectHomeLink.click() await projectHomeLink.click()
await u.waitForPageLoad()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
}) })
@ -878,7 +926,7 @@ test.describe(`Project management commands`, () => {
test( test(
`Rename from home page`, `Rename from home page`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page, homePage, scene, cmdBar }, testInfo) => { async ({ context, page, homePage }, testInfo) => {
const projectName = `my_project_to_rename` const projectName = `my_project_to_rename`
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
@ -893,7 +941,7 @@ test.describe(`Project management commands`, () => {
const commandButton = page.getByRole('button', { name: 'Commands' }) const commandButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', { name: 'rename project' }) const commandOption = page.getByRole('option', { name: 'rename project' })
const projectNameOption = page.getByRole('option', { name: projectName }) const projectNameOption = page.getByRole('option', { name: projectName })
const projectRenamedName = `untitled` const projectRenamedName = `project-000`
const commandContinueButton = page.getByRole('button', { const commandContinueButton = page.getByRole('button', {
name: 'Continue', name: 'Continue',
}) })
@ -934,7 +982,7 @@ test.describe(`Project management commands`, () => {
test( test(
`Delete from home page`, `Delete from home page`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page, scene, cmdBar }, testInfo) => { async ({ context, page }, testInfo) => {
const projectName = `my_project_to_delete` const projectName = `my_project_to_delete`
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
@ -985,7 +1033,6 @@ test.describe(`Project management commands`, () => {
homePage, homePage,
toolbar, toolbar,
cmdBar, cmdBar,
scene,
}) => { }) => {
const projectName = 'test-project' const projectName = 'test-project'
await test.step(`Setup`, async () => { await test.step(`Setup`, async () => {
@ -1025,11 +1072,10 @@ test.describe(`Project management commands`, () => {
}) })
await cmdBar.argumentInput.fill(projectName) await cmdBar.argumentInput.fill(projectName)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await scene.settled(cmdBar)
await toolbar.logoLink.click()
}) })
await test.step(`Check the project was created with a non-colliding name`, async () => { await test.step(`Check the project was created with a non-colliding name`, async () => {
await toolbar.logoLink.click()
await homePage.expectState({ await homePage.expectState({
projectCards: [ projectCards: [
{ {
@ -1060,11 +1106,10 @@ test.describe(`Project management commands`, () => {
}) })
await cmdBar.argumentInput.fill(projectName) await cmdBar.argumentInput.fill(projectName)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await scene.settled(cmdBar)
await toolbar.logoLink.click()
}) })
await test.step(`Check the second project was created with a non-colliding name`, async () => { await test.step(`Check the second project was created with a non-colliding name`, async () => {
await toolbar.logoLink.click()
await homePage.expectState({ await homePage.expectState({
projectCards: [ projectCards: [
{ {
@ -1094,7 +1139,7 @@ test(`Create a few projects using the default project name`, async ({
await test.step(`Create project ${i}`, async () => { await test.step(`Create project ${i}`, async () => {
await homePage.expectState({ await homePage.expectState({
projectCards: Array.from({ length: i }, (_, i) => ({ projectCards: Array.from({ length: i }, (_, i) => ({
title: i === 0 ? 'untitled' : `untitled-${i}`, title: `project-${i.toString().padStart(3, '0')}`,
fileCount: 1, fileCount: 1,
})).toReversed(), })).toReversed(),
sortBy: 'last-modified-desc', sortBy: 'last-modified-desc',
@ -1150,7 +1195,7 @@ test(
test( test(
'Nested directories in project without main.kcl do not create main.kcl', 'Nested directories in project without main.kcl do not create main.kcl',
{ tag: '@electron' }, { tag: '@electron' },
async ({ scene, cmdBar, context, page }, testInfo) => { async ({ context, page }, testInfo) => {
let testDir: string | undefined let testDir: string | undefined
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
await fsp.mkdir(path.join(dir, 'router-template-slate', 'nested'), { await fsp.mkdir(path.join(dir, 'router-template-slate', 'nested'), {
@ -1173,7 +1218,10 @@ test(
await test.step('Open the project', async () => { await test.step('Open the project', async () => {
await page.getByText('router-template-slate').click() await page.getByText('router-template-slate').click()
await scene.settled(cmdBar) await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
// It actually loads. // It actually loads.
await expect(u.codeLocator).toContainText('mounting bracket') await expect(u.codeLocator).toContainText('mounting bracket')
@ -1275,9 +1323,9 @@ test(
}) })
await test.step('Check we can still create a project', async () => { await test.step('Check we can still create a project', async () => {
await createProject({ name: 'new-project', page, returnHome: true }) await createProject({ name: 'project-000', page, returnHome: true })
await expect( await expect(
page.getByTestId('project-link').filter({ hasText: 'new-project' }) page.getByTestId('project-link').filter({ hasText: 'project-000' })
).toBeVisible() ).toBeVisible()
}) })
} }
@ -1286,7 +1334,7 @@ test(
test( test(
'Can load a file with CRLF line endings', 'Can load a file with CRLF line endings',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page, scene, cmdBar }, testInfo) => { async ({ context, page }, testInfo) => {
if (runningOnWindows()) { if (runningOnWindows()) {
test.fixme(orRunWhenFullSuiteEnabled()) test.fixme(orRunWhenFullSuiteEnabled())
} }
@ -1309,8 +1357,13 @@ test(
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log)
await page.getByText('router-template-slate').click() await page.getByText('router-template-slate').click()
await scene.settled(cmdBar) await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(u.codeLocator).toContainText('routerDiameter') await expect(u.codeLocator).toContainText('routerDiameter')
await expect(u.codeLocator).toContainText('templateGap') await expect(u.codeLocator).toContainText('templateGap')
@ -1454,12 +1507,7 @@ test(
await u.waitForPageLoad() await u.waitForPageLoad()
// The file should be prepopulated with the user's unit settings. await page.locator('.cm-content').fill(`sketch001 = startSketchOn(XZ)
await expect(page.locator('.cm-content')).toHaveText(
'@settings(defaultLengthUnit = in)'
)
await page.locator('.cm-content').fill(`sketch001 = startSketchOn('XZ')
|> startProfileAt([-87.4, 282.92], %) |> startProfileAt([-87.4, 282.92], %)
|> line(end = [324.07, 27.199], tag = $seg01) |> line(end = [324.07, 27.199], tag = $seg01)
|> line(end = [118.328, -291.754]) |> line(end = [118.328, -291.754])
@ -1525,7 +1573,7 @@ extrude001 = extrude(sketch001, length = 200)`)
test( test(
'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)', 'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page, cmdBar, homePage, scene }, testInfo) => { async ({ context, page, cmdBar, homePage }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
await fsp.mkdir(path.join(dir, 'router-template-slate'), { await fsp.mkdir(path.join(dir, 'router-template-slate'), {
recursive: true, recursive: true,
@ -1554,10 +1602,13 @@ test(
path.join(dir, 'bracket', 'main.kcl') path.join(dir, 'bracket', 'main.kcl')
) )
}) })
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project via command palette should load the stream', async () => { await test.step('Opening the bracket project via command palette should load the stream', async () => {
await homePage.expectState({ await homePage.expectState({
projectCards: [ projectCards: [
@ -1591,7 +1642,15 @@ test(
stage: 'commandBarClosed', stage: 'commandBarClosed',
}) })
await scene.settled(cmdBar) await u.waitForPageLoad()
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
}) })
await test.step('Clicking the logo takes us back to the projects page / home', async () => { await test.step('Clicking the logo takes us back to the projects page / home', async () => {
@ -1608,7 +1667,15 @@ test(
await page.getByText('router-template-slate').click() await page.getByText('router-template-slate').click()
await scene.settled(cmdBar) await u.waitForPageLoad()
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
timeout: 10_000,
})
.toBeLessThan(15)
}) })
await test.step('The projects on the home page should still be normal', async () => { await test.step('The projects on the home page should still be normal', async () => {
@ -1661,6 +1728,8 @@ test(
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log)
// we'll grab this from the settings on screen before we switch // we'll grab this from the settings on screen before we switch
let originalProjectDirName: string let originalProjectDirName: string
const newProjectDirName = testInfo.outputPath( const newProjectDirName = testInfo.outputPath(
@ -1801,7 +1870,7 @@ test(
test( test(
'file pane is scrollable when there are many files', 'file pane is scrollable when there are many files',
{ tag: '@electron' }, { tag: '@electron' },
async ({ scene, cmdBar, context, page }, testInfo) => { async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const testDir = path.join(dir, 'testProject') const testDir = path.join(dir, 'testProject')
await fsp.mkdir(testDir, { recursive: true }) await fsp.mkdir(testDir, { recursive: true })
@ -1880,8 +1949,10 @@ test(
await test.step('setup, open file pane', async () => { await test.step('setup, open file pane', async () => {
await page.getByText('testProject').click() await page.getByText('testProject').click()
await expect(page.getByTestId('loading')).toBeAttached()
await scene.settled(cmdBar) await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await page.getByTestId('files-pane-button').click() await page.getByTestId('files-pane-button').click()
}) })

View File

@ -1,5 +1,4 @@
import { expect, test } from '@e2e/playwright/zoo-test' import { test, expect } from './zoo-test'
/* eslint-disable jest/no-conditional-expect */ /* eslint-disable jest/no-conditional-expect */
/** /**
@ -63,7 +62,7 @@ test.describe('edit with AI example snapshots', () => {
localStorage.setItem('persistCode', file) localStorage.setItem('persistCode', file)
}, file) }, file)
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await scene.waitForExecutionDone()
const body1CapCoords = { x: 571, y: 351 } const body1CapCoords = { x: 571, y: 351 }
const [clickBody1Cap] = scene.makeMouseHelpers( const [clickBody1Cap] = scene.makeMouseHelpers(

View File

@ -1,5 +1,5 @@
import { orRunWhenFullSuiteEnabled } from '@e2e/playwright/test-utils' import { test, expect } from './zoo-test'
import { expect, test } from '@e2e/playwright/zoo-test' import { orRunWhenFullSuiteEnabled } from './test-utils'
/* eslint-disable jest/no-conditional-expect */ /* eslint-disable jest/no-conditional-expect */
@ -61,7 +61,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
localStorage.setItem('persistCode', file) localStorage.setItem('persistCode', file)
}, file) }, file)
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await scene.waitForExecutionDone()
const body1CapCoords = { x: 571, y: 311 } const body1CapCoords = { x: 571, y: 311 }
const greenCheckCoords = { x: 565, y: 305 } const greenCheckCoords = { x: 565, y: 305 }
@ -156,7 +156,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
localStorage.setItem('persistCode', file) localStorage.setItem('persistCode', file)
}, file) }, file)
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await scene.waitForExecutionDone()
const body1CapCoords = { x: 571, y: 311 } const body1CapCoords = { x: 571, y: 311 }
const [clickBody1Cap] = scene.makeMouseHelpers( const [clickBody1Cap] = scene.makeMouseHelpers(
@ -212,7 +212,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
localStorage.setItem('persistCode', file) localStorage.setItem('persistCode', file)
}, file) }, file)
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await scene.waitForExecutionDone()
const submittingToast = page.getByText('Submitting to Text-to-CAD API...') const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
const successToast = page.getByText('Prompt to edit successful') const successToast = page.getByText('Prompt to edit successful')
@ -281,7 +281,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
localStorage.setItem('persistCode', file) localStorage.setItem('persistCode', file)
}, file) }, file)
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await scene.waitForExecutionDone()
const submittingToast = page.getByText('Submitting to Text-to-CAD API...') const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
const successToast = page.getByText('Prompt to edit successful') const successToast = page.getByText('Prompt to edit successful')

View File

@ -1,18 +1,17 @@
import type { Page } from '@playwright/test' import { Page } from '@playwright/test'
import { bracket } from '@src/lib/exampleKcl' import { test, expect } from './zoo-test'
import { reportRejection } from '@src/lib/trap'
import * as fsp from 'fs/promises'
import path from 'path' import path from 'path'
import * as fsp from 'fs/promises'
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from '@e2e/playwright/storageStates'
import type { TestColor } from '@e2e/playwright/test-utils'
import { import {
TEST_COLORS,
executorInputPath,
getUtils, getUtils,
executorInputPath,
TEST_COLORS,
TestColor,
orRunWhenFullSuiteEnabled, orRunWhenFullSuiteEnabled,
} from '@e2e/playwright/test-utils' } from './test-utils'
import { expect, test } from '@e2e/playwright/zoo-test' import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
import { bracket } from 'lib/exampleKcl'
import { reportRejection } from 'lib/trap'
test.describe('Regression tests', { tag: ['@skipWin'] }, () => { test.describe('Regression tests', { tag: ['@skipWin'] }, () => {
// bugs we found that don't fit neatly into other categories // bugs we found that don't fit neatly into other categories
@ -332,7 +331,7 @@ extrude001 = extrude(sketch001, length = 50)
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit = mm) `@settings(defaultLengthUnit = mm)
sketch002 = startSketchOn(XY) sketch002 = startSketchOn('XY')
profile002 = startProfileAt([72.24, -52.05], sketch002) profile002 = startProfileAt([72.24, -52.05], sketch002)
|> angledLine([0, 181.26], %, $rectangleSegmentA001) |> angledLine([0, 181.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
@ -583,7 +582,7 @@ extrude002 = extrude(profile002, length = 150)
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('cylinder-inches.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
path.join(bracketDir, 'main.kcl') path.join(bracketDir, 'main.kcl')
) )
}) })
@ -620,7 +619,6 @@ extrude002 = extrude(profile002, length = 150)
test(`View gizmo stays visible even when zoomed out all the way`, async ({ test(`View gizmo stays visible even when zoomed out all the way`, async ({
page, page,
homePage, homePage,
scene,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -634,7 +632,7 @@ extrude002 = extrude(profile002, length = 150)
await test.step(`Load an empty file`, async () => { await test.step(`Load an empty file`, async () => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem('persistCode', '@settings(defaultLengthUnit = in)') localStorage.setItem('persistCode', '')
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
@ -648,31 +646,22 @@ extrude002 = extrude(profile002, length = 150)
timeout: 5000, timeout: 5000,
message: 'Plane color is visible', message: 'Plane color is visible',
}) })
.toBeLessThanOrEqual(20) .toBeLessThanOrEqual(15)
await expect(scene.startEditSketchBtn).toBeEnabled()
let maxZoomOuts = 10 let maxZoomOuts = 10
let middlePixelIsBackgroundColor = let middlePixelIsBackgroundColor =
(await middlePixelIsColor(bgColor)) < 10 (await middlePixelIsColor(bgColor)) < 10
console.time('pressing control')
await page.keyboard.down('Control')
while (!middlePixelIsBackgroundColor && maxZoomOuts > 0) { while (!middlePixelIsBackgroundColor && maxZoomOuts > 0) {
await page.waitForTimeout(100) await page.keyboard.down('Control')
await page.mouse.move(650, 460) await page.mouse.move(600, 460)
console.time('moved to start point')
await page.mouse.down({ button: 'right' }) await page.mouse.down({ button: 'right' })
console.time('moused down') await page.mouse.move(600, 50, { steps: 20 })
await page.mouse.move(650, 50, { steps: 20 })
console.time('moved to end point')
await page.waitForTimeout(100)
await page.mouse.up({ button: 'right' }) await page.mouse.up({ button: 'right' })
console.time('moused up')
maxZoomOuts--
middlePixelIsBackgroundColor = (await middlePixelIsColor(bgColor)) < 15
}
await page.keyboard.up('Control') await page.keyboard.up('Control')
await page.waitForTimeout(100)
maxZoomOuts--
middlePixelIsBackgroundColor = (await middlePixelIsColor(bgColor)) < 10
}
expect(middlePixelIsBackgroundColor, { expect(middlePixelIsBackgroundColor, {
message: 'We should not see the default planes', message: 'We should not see the default planes',
@ -695,7 +684,7 @@ extrude002 = extrude(profile002, length = 150)
const legoDir = path.join(dir, 'lego') const legoDir = path.join(dir, 'lego')
await fsp.mkdir(legoDir, { recursive: true }) await fsp.mkdir(legoDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('e2e-can-sketch-on-chamfer.kcl'), executorInputPath('lego.kcl'),
path.join(legoDir, 'main.kcl') path.join(legoDir, 'main.kcl')
) )
}) })
@ -704,12 +693,15 @@ extrude002 = extrude(profile002, length = 150)
await homePage.openProject('lego') await homePage.openProject('lego')
await toolbar.closePane('code') await toolbar.closePane('code')
}) })
await test.step(`Waiting for scene to settle`, async () => { await test.step(`Waiting for the loading spinner to disappear`, async () => {
await scene.connectionEstablished() await scene.loadingIndicator.waitFor({ state: 'detached' })
}) })
await test.step(`The part should start loading quickly, not waiting until execution is complete`, async () => { await test.step(`The part should start loading quickly, not waiting until execution is complete`, async () => {
// TODO: use the viewport size to pick the center point, but the `viewport` fixture's values were wrong. await scene.expectPixelColor(
await scene.expectPixelColor([116, 116, 116], { x: 500, y: 250 }, 15) [143, 143, 143],
{ x: (viewport?.width ?? 1200) / 2, y: (viewport?.height ?? 500) / 2 },
15
)
}) })
}) })
@ -763,7 +755,7 @@ plane002 = offsetPlane(XZ, offset = -2 * x)`
) )
}) })
await homePage.openProject('test-sample') await homePage.openProject('test-sample')
await scene.settled(cmdBar) await scene.waitForExecutionDone()
await expect(toolbar.startSketchBtn).toBeEnabled({ timeout: 20_000 }) await expect(toolbar.startSketchBtn).toBeEnabled({ timeout: 20_000 })
const operationButton = await toolbar.getFeatureTreeOperation( const operationButton = await toolbar.getFeatureTreeOperation(
'Offset Plane', 'Offset Plane',
@ -799,74 +791,6 @@ plane002 = offsetPlane(XZ, offset = -2 * x)`
await page.getByTestId('custom-cmd-send-button').click() await page.getByTestId('custom-cmd-send-button').click()
} }
) )
test('scale other than default works with sketch mode', async ({
page,
homePage,
toolbar,
editor,
scene,
}) => {
await test.step('Load the washer code', async () => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit = in)
innerDiameter = 0.203
outerDiameter = 0.438
thicknessMax = 0.038
thicknessMin = 0.024
washerSketch = startSketchOn(XY)
|> circle(center = [0, 0], radius = outerDiameter / 2)
washer = extrude(washerSketch, length = thicknessMax)`
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
})
const [circleCenterClick] = scene.makeMouseHelpers(650, 300)
const [circleRadiusClick] = scene.makeMouseHelpers(800, 320)
const [washerFaceClick] = scene.makeMouseHelpers(657, 286)
await page.waitForTimeout(100)
await test.step('Start sketching on the washer face', async () => {
await toolbar.startSketchPlaneSelection()
await washerFaceClick()
await page.waitForTimeout(600) // engine animation
await toolbar.expectToolbarMode.toBe('sketching')
})
await test.step('Draw a circle and verify code', async () => {
// select circle tool
await expect
.poll(async () => {
await toolbar.circleBtn.click()
return toolbar.circleBtn.getAttribute('aria-pressed')
})
.toBe('true')
await page.waitForTimeout(100)
await circleCenterClick()
// this number will be different if the scale is not set correctly for inches
await editor.expectEditor.toContain(
'circle(sketch001, center = [0.06, -0.06]'
)
await circleRadiusClick()
await editor.expectEditor.toContain(
'circle(sketch001, center = [0.06, -0.06], radius = 0.18'
)
})
await test.step('Exit sketch mode', async () => {
await toolbar.exitSketch()
await toolbar.expectToolbarMode.toBe('modeling')
await toolbar.selectUnit('Yards')
await editor.expectEditor.toContain('@settings(defaultLengthUnit = yd)')
})
})
}) })
async function clickExportButton(page: Page) { async function clickExportButton(page: Page) {

View File

@ -1,20 +1,20 @@
import type { Page } from '@playwright/test' import { Page } from '@playwright/test'
import { roundOff, uuidv4 } from '@src/lib/utils' import { test, expect } from './zoo-test'
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
import { HomePageFixture } from './fixtures/homePageFixture'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import type { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
import { import {
PERSIST_MODELING_CONTEXT,
TEST_COLORS,
getMovementUtils, getMovementUtils,
getUtils, getUtils,
PERSIST_MODELING_CONTEXT,
TEST_COLORS,
orRunWhenFullSuiteEnabled, orRunWhenFullSuiteEnabled,
} from '@e2e/playwright/test-utils' } from './test-utils'
import { expect, test } from '@e2e/playwright/zoo-test' import { uuidv4, roundOff } from 'lib/utils'
import { SceneFixture } from './fixtures/sceneFixture'
import { ToolbarFixture } from './fixtures/toolbarFixture'
import { CmdBarFixture } from './fixtures/cmdBarFixture'
test.describe('Sketch tests', { tag: ['@skipWin'] }, () => { test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
test('multi-sketch file shows multiple Edit Sketch buttons', async ({ test('multi-sketch file shows multiple Edit Sketch buttons', async ({
@ -22,7 +22,6 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
context, context,
homePage, homePage,
scene, scene,
cmdBar,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
const selectionsSnippets = { const selectionsSnippets = {
@ -83,7 +82,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await scene.waitForExecutionDone()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -109,14 +108,12 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
page, page,
scene, scene,
homePage, homePage,
cmdBar,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit = in) `sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
|> startProfileAt([2.61, -4.01], %) |> startProfileAt([2.61, -4.01], %)
|> xLine(length = 8.73) |> xLine(length = 8.73)
|> tangentialArcTo([8.33, -1.31], %)` |> tangentialArcTo([8.33, -1.31], %)`
@ -124,7 +121,7 @@ sketch001 = startSketchOn(XZ)
}) })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await scene.waitForExecutionDone()
await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 587, y: 270 }, 15) await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 587, y: 270 }, 15)
@ -162,10 +159,7 @@ sketch001 = startSketchOn(XZ)
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await expect.poll(u.normalisedEditorCode, { timeout: 1000 }) await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
.toBe(`@settings(defaultLengthUnit = in) .toBe(`sketch002 = startSketchOn(XZ)
sketch002 = startSketchOn(XZ)
sketch001 = startProfileAt([12.34, -12.34], sketch002) sketch001 = startProfileAt([12.34, -12.34], sketch002)
|> yLine(length = 12.34) |> yLine(length = 12.34)
@ -481,8 +475,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit=in) `sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
|> circle(center = [4.61, -5.01], radius = 8)` |> circle(center = [4.61, -5.01], radius = 8)`
) )
}) })
@ -567,14 +560,12 @@ sketch001 = startSketchOn(XZ)
test('Can edit a sketch that has been extruded in the same pipe', async ({ test('Can edit a sketch that has been extruded in the same pipe', async ({
page, page,
homePage, homePage,
editor,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit=in) `sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
|> startProfileAt([4.61, -10.01], %) |> startProfileAt([4.61, -10.01], %)
|> line(end = [12.73, -0.09]) |> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
@ -659,30 +650,26 @@ sketch001 = startSketchOn(XZ)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed // expect the code to have changed
await editor.expectEditor.toContain( await expect(page.locator('.cm-content'))
`sketch001 = startSketchOn(XZ) .toHaveText(`sketch001 = startSketchOn(XZ)
|> startProfileAt([7.12, -12.68], %) |> startProfileAt([7.12, -12.68], %)
|> line(end = [12.68, -1.09]) |> line(end = [12.68, -1.09])
|> tangentialArcTo([24.89, 0.68], %) |> tangentialArcTo([24.89, 0.68], %)
|> close() |> close()
|> extrude(length = 5)`, |> extrude(length = 5)
{ shouldNormalise: true } `)
)
}) })
test('Can edit a sketch that has been revolved in the same pipe', async ({ test('Can edit a sketch that has been revolved in the same pipe', async ({
page, page,
homePage, homePage,
scene, scene,
editor,
cmdBar,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit=in) `sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
|> startProfileAt([4.61, -14.01], %) |> startProfileAt([4.61, -14.01], %)
|> line(end = [12.73, -0.09]) |> line(end = [12.73, -0.09])
|> tangentialArcTo([24.95, -5.38], %) |> tangentialArcTo([24.95, -5.38], %)
@ -692,7 +679,7 @@ sketch001 = startSketchOn(XZ)
}) })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await scene.waitForExecutionDone()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
@ -767,16 +754,14 @@ sketch001 = startSketchOn(XZ)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed // expect the code to have changed
await editor.expectEditor.toContain( await expect(page.locator('.cm-content'))
`sketch001 = startSketchOn(XZ) .toHaveText(`sketch001 = startSketchOn(XZ)
|> startProfileAt([6.44, -12.07], %) |> startProfileAt([6.44, -12.07], %)
|> line(end = [14.72, 1.97]) |> line(end = [14.72, 1.97])
|> tangentialArcTo([24.95, -5.38], %) |> tangentialArcTo([24.95, -5.38], %)
|> line(end = [1.97, 2.06]) |> line(end = [1.97, 2.06])
|> close() |> close()
|> revolve(axis = X)`, |> revolve(axis = X)`)
{ shouldNormalise: true }
)
}) })
test('Can add multiple sketches', async ({ page, homePage }) => { test('Can add multiple sketches', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -804,8 +789,7 @@ sketch001 = startSketchOn(XZ)
200 200
) )
let codeStr = let codeStr = 'sketch001 = startSketchOn(XY)'
'@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XY)'
await page.mouse.click(center.x, viewportSize.height * 0.55) await page.mouse.click(center.x, viewportSize.height * 0.55)
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
@ -884,8 +868,7 @@ sketch001 = startSketchOn(XZ)
await u.openDebugPanel() await u.openDebugPanel()
const code = `@settings(defaultLengthUnit = in) const code = `sketch001 = startSketchOn(-XZ)
sketch001 = startSketchOn(-XZ)
profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff( profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
scale * 34.8 scale * 34.8
)}], sketch001) )}], sketch001)
@ -915,7 +898,7 @@ profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
await page.mouse.move(700, 200, { steps: 10 }) await page.mouse.move(700, 200, { steps: 10 })
await page.mouse.click(700, 200, { delay: 200 }) await page.mouse.click(700, 200, { delay: 200 })
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(-XZ)` `sketch001 = startSketchOn(-XZ)`
) )
let prevContent = await page.locator('.cm-content').innerText() let prevContent = await page.locator('.cm-content').innerText()
@ -1443,8 +1426,7 @@ test.describe(`Sketching with offset planes`, () => {
await context.addInitScript(() => { await context.addInitScript(() => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit = in) `offsetPlane001 = offsetPlane(XY, offset = 10)`
offsetPlane001 = offsetPlane(XY, offset = 10)`
) )
}) })
@ -1458,7 +1440,7 @@ offsetPlane001 = offsetPlane(XY, offset = 10)`
await test.step(`Hovering should highlight code`, async () => { await test.step(`Hovering should highlight code`, async () => {
await planeHover() await planeHover()
await editor.expectState({ await editor.expectState({
activeLines: [`@settings(defaultLengthUnit = in)`], activeLines: [`offsetPlane001=offsetPlane(XY,offset=10)`],
diagnostics: [], diagnostics: [],
highlightedCode: 'offsetPlane(XY, offset = 10)', highlightedCode: 'offsetPlane(XY, offset = 10)',
}) })
@ -1471,7 +1453,7 @@ offsetPlane001 = offsetPlane(XY, offset = 10)`
await expect(toolbar.lineBtn).toBeEnabled() await expect(toolbar.lineBtn).toBeEnabled()
await editor.expectEditor.toContain('startSketchOn(offsetPlane001)') await editor.expectEditor.toContain('startSketchOn(offsetPlane001)')
await editor.expectState({ await editor.expectState({
activeLines: [`@settings(defaultLengthUnit = in)`], activeLines: [`offsetPlane001=offsetPlane(XY,offset=10)`],
diagnostics: [], diagnostics: [],
highlightedCode: '', highlightedCode: '',
}) })
@ -1617,13 +1599,12 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
test( test(
`snapToProfile start only works for current profile`, `snapToProfile start only works for current profile`,
{ tag: ['@skipWin'] }, { tag: ['@skipWin'] },
async ({ context, page, scene, toolbar, editor, homePage, cmdBar }) => { async ({ context, page, scene, toolbar, editor, homePage }) => {
// We seed the scene with a single offset plane // We seed the scene with a single offset plane
await context.addInitScript(() => { await context.addInitScript(() => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit = in) `sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
profile002 = startProfileAt([40.68, 87.67], sketch001) profile002 = startProfileAt([40.68, 87.67], sketch001)
|> xLine(length = 239.17) |> xLine(length = 239.17)
profile003 = startProfileAt([206.63, -56.73], sketch001) profile003 = startProfileAt([206.63, -56.73], sketch001)
@ -1633,8 +1614,6 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
}) })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar)
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -1656,13 +1635,9 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
const codeFromTangentialArc = ` |> tangentialArcTo([39.49, 88.22], %)` const codeFromTangentialArc = ` |> tangentialArcTo([39.49, 88.22], %)`
await test.step('check that tangential tool does not snap to other profile starts', async () => { await test.step('check that tangential tool does not snap to other profile starts', async () => {
await toolbar.tangentialArcBtn.click() await toolbar.tangentialArcBtn.click()
await page.waitForTimeout(1000)
await endOfLowerSegMove() await endOfLowerSegMove()
await page.waitForTimeout(1000)
await endOfLowerSegClick() await endOfLowerSegClick()
await page.waitForTimeout(1000)
await profileStartOfHigherSegClick() await profileStartOfHigherSegClick()
await page.waitForTimeout(1000)
await editor.expectEditor.toContain(codeFromTangentialArc) await editor.expectEditor.toContain(codeFromTangentialArc)
await editor.expectEditor.not.toContain( await editor.expectEditor.not.toContain(
`[profileStartX(%), profileStartY(%)]` `[profileStartX(%), profileStartY(%)]`
@ -2197,8 +2172,7 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit = in) `sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([6.24, 4.54], sketch001) profile001 = startProfileAt([6.24, 4.54], sketch001)
|> line(end = [-0.41, 6.99]) |> line(end = [-0.41, 6.99])
|> line(end = [8.61, 0.74]) |> line(end = [8.61, 0.74])
@ -2251,9 +2225,8 @@ profile004 = circleThreePoint(sketch001, p1 = [13.44, -6.8], p2 = [13.39, -2.07]
await test.step('enter sketch and setup', async () => { await test.step('enter sketch and setup', async () => {
await moveToClearToolBarPopover() await moveToClearToolBarPopover()
await page.waitForTimeout(1000)
await pointOnSegment({ shouldDbClick: true }) await pointOnSegment({ shouldDbClick: true })
await page.waitForTimeout(2000) await page.waitForTimeout(600)
await toolbar.lineBtn.click() await toolbar.lineBtn.click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -2344,8 +2317,7 @@ profile004 = circleThreePoint(sketch001, p1 = [13.44, -6.8], p2 = [13.39, -2.07]
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit = in) `sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([6.24, 4.54], sketch001) profile001 = startProfileAt([6.24, 4.54], sketch001)
|> line(end = [-0.41, 6.99]) |> line(end = [-0.41, 6.99])
|> line(end = [8.61, 0.74]) |> line(end = [8.61, 0.74])
@ -2369,7 +2341,7 @@ profile003 = circle(sketch001, center = [6.92, -4.2], radius = 3.16)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await scene.waitForExecutionDone()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -2450,8 +2422,7 @@ profile003 = circle(sketch001, center = [6.92, -4.2], radius = 3.16)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit = in) `sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([-63.43, 193.08], sketch001) profile001 = startProfileAt([-63.43, 193.08], sketch001)
|> line(end = [168.52, 149.87]) |> line(end = [168.52, 149.87])
|> line(end = [190.29, -39.18]) |> line(end = [190.29, -39.18])
@ -2515,11 +2486,7 @@ extrude001 = extrude(profile003, length = 5)
page, page,
}) => { }) => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem('persistCode', `myVar = 5`)
'persistCode',
`@settings(defaultLengthUnit = in)
myVar = 5`
)
}) })
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
@ -2566,8 +2533,7 @@ extrude001 = extrude(profile003, length = 5)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit = in) `sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([85.19, 338.59], sketch001) profile001 = startProfileAt([85.19, 338.59], sketch001)
|> line(end = [213.3, -94.52]) |> line(end = [213.3, -94.52])
|> line(end = [-230.09, -55.34]) |> line(end = [-230.09, -55.34])
@ -2609,8 +2575,7 @@ profile002 = startProfileAt([85.81, 52.55], sketch002)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit = in) `thePart = startSketchOn(XZ)
thePart = startSketchOn(XZ)
|> startProfileAt([7.53, 10.51], %) |> startProfileAt([7.53, 10.51], %)
|> line(end = [12.54, 1.83]) |> line(end = [12.54, 1.83])
|> line(end = [6.65, -6.91]) |> line(end = [6.65, -6.91])
@ -2671,8 +2636,7 @@ extrude001 = extrude(thePart, length = 75)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit = in) `sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([6.71, -3.66], sketch001) profile001 = startProfileAt([6.71, -3.66], sketch001)
|> line(end = [2.65, 9.02], tag = $seg02) |> line(end = [2.65, 9.02], tag = $seg02)
|> line(end = [3.73, -9.36], tag = $seg01) |> line(end = [3.73, -9.36], tag = $seg01)
@ -2845,8 +2809,7 @@ extrude003 = extrude(profile011, length = 2.5)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`@settings(defaultLengthUnit = in) `sketch001 = startSketchOn(XZ)
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([34, 42.66], sketch001) profile001 = startProfileAt([34, 42.66], sketch001)
|> line(end = [102.65, 151.99]) |> line(end = [102.65, 151.99])
|> line(end = [76, -138.66]) |> line(end = [76, -138.66])
@ -2975,7 +2938,6 @@ test.describe(`Click based selection don't brick the app when clicked out of ran
toolbar, toolbar,
editor, editor,
homePage, homePage,
cmdBar,
}) => { }) => {
// We seed the scene with a single offset plane // We seed the scene with a single offset plane
await context.addInitScript(() => { await context.addInitScript(() => {
@ -2993,7 +2955,7 @@ test.describe(`Click based selection don't brick the app when clicked out of ran
}) })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await scene.waitForExecutionDone()
await test.step(`format the code`, async () => { await test.step(`format the code`, async () => {
// doesn't contain condensed version // doesn't contain condensed version
@ -3058,7 +3020,6 @@ test.describe('Redirecting to home page and back to the original file should cle
toolbar, toolbar,
editor, editor,
homePage, homePage,
cmdBar,
}) => { }) => {
// We seed the scene with a single offset plane // We seed the scene with a single offset plane
await context.addInitScript(() => { await context.addInitScript(() => {
@ -3071,7 +3032,7 @@ test.describe('Redirecting to home page and back to the original file should cle
) )
}) })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.settled(cmdBar) await scene.waitForExecutionDone()
const [objClick] = scene.makeMouseHelpers(634, 274) const [objClick] = scene.makeMouseHelpers(634, 274)
await objClick() await objClick()

View File

@ -1,22 +1,21 @@
import type { Models } from '@kittycad/lib' import { test, expect } from './zoo-test'
import { KCL_DEFAULT_LENGTH } from '@src/lib/constants' import { secrets } from './secrets'
import { spawn } from 'child_process'
import fsp from 'fs/promises'
import JSZip from 'jszip'
import path from 'path'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
import { secrets } from '@e2e/playwright/secrets'
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
import type { Paths } from '@e2e/playwright/test-utils'
import { import {
Paths,
doExport, doExport,
getUtils, getUtils,
orRunWhenFullSuiteEnabled,
settingsToToml, settingsToToml,
} from '@e2e/playwright/test-utils' orRunWhenFullSuiteEnabled,
import { expect, test } from '@e2e/playwright/zoo-test' } from './test-utils'
import { Models } from '@kittycad/lib'
import fsp from 'fs/promises'
import { spawn } from 'child_process'
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import JSZip from 'jszip'
import path from 'path'
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from './storageStates'
import { SceneFixture } from './fixtures/sceneFixture'
import { CmdBarFixture } from './fixtures/cmdBarFixture'
test.beforeEach(async ({ page, context }) => { test.beforeEach(async ({ page, context }) => {
// Make the user avatar image always 404 // Make the user avatar image always 404
@ -103,6 +102,7 @@ part001 = startSketchOn(-XZ)
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
const axisDirectionPair: Models['AxisDirectionPair_type'] = { const axisDirectionPair: Models['AxisDirectionPair_type'] = {
@ -345,12 +345,10 @@ const extrudeDefaultPlane = async (
app: { app: {
onboarding_status: 'dismissed', onboarding_status: 'dismissed',
show_debug_panel: true, show_debug_panel: true,
appearance: {
theme: 'dark', theme: 'dark',
}, },
},
project: { project: {
default_project_name: 'untitled', default_project_name: 'project-$nnn',
}, },
text_editor: { text_editor: {
text_wrapping: true, text_wrapping: true,
@ -368,6 +366,7 @@ const extrudeDefaultPlane = async (
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
@ -419,6 +418,8 @@ test(
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
const startXPx = 600 const startXPx = 600
const [endOfTangentClk, endOfTangentMv] = scene.makeMouseHelpers( const [endOfTangentClk, endOfTangentMv] = scene.makeMouseHelpers(
startXPx + PUR * 30, startXPx + PUR * 30,
@ -451,7 +452,7 @@ test(
await page.waitForTimeout(700) // TODO detect animation ending, or disable animation await page.waitForTimeout(700) // TODO detect animation ending, or disable animation
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `profile001 = startProfileAt([182.59, -246.32], sketch001)` code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
await expect(page.locator('.cm-content')).toHaveText(code) await expect(page.locator('.cm-content')).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -469,7 +470,7 @@ test(
await page.waitForTimeout(500) await page.waitForTimeout(500)
code += ` code += `
|> xLine(length = 184.3)` |> xLine(length = 7.25)`
await expect(page.locator('.cm-content')).toHaveText(code) await expect(page.locator('.cm-content')).toHaveText(code)
await page await page
@ -547,6 +548,8 @@ test(
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
// click on "Start Sketch" button // click on "Start Sketch" button
await u.doAndWaitForImageDiff( await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(), () => page.getByRole('button', { name: 'Start Sketch' }).click(),
@ -592,6 +595,8 @@ test(
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await u.doAndWaitForImageDiff( await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(), () => page.getByRole('button', { name: 'Start Sketch' }).click(),
200 200
@ -623,7 +628,7 @@ test(
mask: [page.getByTestId('model-state-indicator')], mask: [page.getByTestId('model-state-indicator')],
}) })
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn(XZ)profile001 = circle(sketch001, center = [366.89, -62.01], radius = 1)` `sketch001 = startSketchOn(XZ)profile001 = circle(sketch001, center = [14.44, -2.44], radius = 1)`
) )
} }
) )
@ -642,6 +647,8 @@ test.describe(
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await u.doAndWaitForImageDiff( await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(), () => page.getByRole('button', { name: 'Start Sketch' }).click(),
200 200
@ -658,7 +665,7 @@ test.describe(
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `profile001 = startProfileAt([182.59, -246.32], sketch001)` code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -666,7 +673,7 @@ test.describe(
await page.waitForTimeout(100) await page.waitForTimeout(100)
code += ` code += `
|> xLine(length = 184.3)` |> xLine(length = 7.25)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
await page await page
@ -681,7 +688,7 @@ test.describe(
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
code += ` code += `
|> tangentialArcTo([551.2, -62.01], %)` |> tangentialArcTo([21.7, -2.44], %)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
// click tangential arc tool again to unequip it // click tangential arc tool again to unequip it
@ -734,6 +741,7 @@ test.describe(
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
await u.doAndWaitForImageDiff( await u.doAndWaitForImageDiff(
@ -835,6 +843,7 @@ part002 = startSketchOn(part001, seg01)
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
// Wait for the second extrusion to appear // Wait for the second extrusion to appear
@ -890,6 +899,7 @@ test(
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
// Wait for the second extrusion to appear // Wait for the second extrusion to appear
@ -930,6 +940,7 @@ test(
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
// Wait for the second extrusion to appear // Wait for the second extrusion to appear
@ -962,6 +973,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
await u.closeKclCodePanel() await u.closeKclCodePanel()
@ -1026,6 +1038,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
await u.closeKclCodePanel() await u.closeKclCodePanel()
@ -1070,6 +1083,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
await u.closeKclCodePanel() await u.closeKclCodePanel()
@ -1188,6 +1202,7 @@ sweepSketch = startSketchOn(XY)
await page.setViewportSize({ width: 1200, height: 1000 }) await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
await expect(page, 'expect small color widget').toHaveScreenshot({ await expect(page, 'expect small color widget').toHaveScreenshot({
@ -1237,6 +1252,7 @@ sweepSketch = startSketchOn(XY)
await page.setViewportSize({ width: 1200, height: 1000 }) await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await scene.connectionEstablished()
await scene.settled(cmdBar) await scene.settled(cmdBar)
await expect(page.locator('.cm-css-color-picker-wrapper')).toBeVisible() await expect(page.locator('.cm-css-color-picker-wrapper')).toBeVisible()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 68 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: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

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