Compare commits
	
		
			130 Commits
		
	
	
		
			pierremtb/
			...
			jtran/fix-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0c2f63b399 | |||
| 363ae10658 | |||
| ac4a6c84cf | |||
| c6fad2e2dc | |||
| 013cb10961 | |||
| 6261083cb1 | |||
| 2b0ba37ed0 | |||
| 96174f3cf6 | |||
| aed62ff912 | |||
| 9334d64608 | |||
| 4fa7d2d8c8 | |||
| 3e615dfdbc | |||
| c9860af29f | |||
| 23a42f0195 | |||
| a77fa639f3 | |||
| 0a5ad7c95b | |||
| 4a654523d2 | |||
| 73a7e2bfd6 | |||
| eb0850fea9 | |||
| 029f76f273 | |||
| 28b5f7080c | |||
| 5b1dcfecd6 | |||
| f89d191425 | |||
| 2f4e4b62a8 | |||
| 5ebd5c8dbb | |||
| a9ceaf2678 | |||
| c8afd3399b | |||
| 5dda4828c6 | |||
| 72acab752c | |||
| 81df38ad1c | |||
| 0576a2bef1 | |||
| 4b2f6b4647 | |||
| 69edaa4183 | |||
| 2eb7c382bf | |||
| 38913ecb98 | |||
| debd06129f | |||
| d38bd342a0 | |||
| f026f10335 | |||
| 895d7ebc6d | |||
| 65edf17a44 | |||
| 0c2a0a8c07 | |||
| 97cef4d16c | |||
| 9358278f7b | |||
| a174e084d4 | |||
| df7246897a | |||
| 0c9f64dd7c | |||
| d2b9d3a058 | |||
| 7e54f08778 | |||
| d9c2dd376e | |||
| 275a2150e7 | |||
| 8b8feb8d68 | |||
| e21ef3f122 | |||
| 66834931aa | |||
| 06c1bcaf2e | |||
| fc7df7ecbe | |||
| 67cb7b33bb | |||
| ea74b94fac | |||
| 529833c63f | |||
| 92da86391a | |||
| e7cb390db4 | |||
| 8a66bbbdbd | |||
| 3c53babb50 | |||
| 474acb1c68 | |||
| 1c941112d7 | |||
| 6f1d718097 | |||
| 36957237c0 | |||
| da9cae98aa | |||
| 9ae025dc56 | |||
| 579ab23d78 | |||
| 4bef33e745 | |||
| ac7bd28c5a | |||
| d478d81156 | |||
| 3d27f0191b | |||
| 30c2acd18a | |||
| a83b4b2145 | |||
| 70b8541038 | |||
| bb51646738 | |||
| c02e31a530 | |||
| 1d06cc7845 | |||
| e0c07eecfe | |||
| c5d42500fa | |||
| e6e47f77f0 | |||
| 662c2485ac | |||
| 9f891deebb | |||
| d08a07a1f8 | |||
| 872b196a86 | |||
| d535a2862d | |||
| 63a3bc7bc6 | |||
| 02055a8b31 | |||
| 93891422f7 | |||
| 7193b4110a | |||
| 76e7d80a55 | |||
| b816df21d2 | |||
| 3630696848 | |||
| f165d19fda | |||
| 3dd98ae1d5 | |||
| a46e0a0fe7 | |||
| 8f9dc06228 | |||
| fa22c14723 | |||
| 1d39983b08 | |||
| da301ba862 | |||
| efe8089b08 | |||
| 49de3b0ac9 | |||
| 2b2ed470c1 | |||
| 96652a0c48 | |||
| 04e586d07b | |||
| fe5f574a77 | |||
| e787495ad0 | |||
| 8bb9be7a5e | |||
| 00892464e8 | |||
| 05ed2a3367 | |||
| 10cc5bce59 | |||
| a32f150fc1 | |||
| ac60082e67 | |||
| d44dc1b21a | |||
| 813962ea4c | |||
| 738443a6ab | |||
| 4b6bbbe2c5 | |||
| 6ff8addc8b | |||
| da05c38b9e | |||
| 191b9b71fd | |||
| 05163fdded | |||
| 7ed26e21c6 | |||
| c668d40efc | |||
| f38c6b90b7 | |||
| 7bc8bae0ec | |||
| 3804aca27e | |||
| b127680f2f | |||
| b7de8e60cf | |||
| 058fccb5e1 | 
| @ -1,3 +1,3 @@ | ||||
| [codespell] | ||||
| ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall | ||||
| skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts | ||||
| ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall | ||||
| 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,tsconfig.tsbuildinfo | ||||
|  | ||||
							
								
								
									
										12
									
								
								.eslintrc
									
									
									
									
									
								
							
							
						
						| @ -5,16 +5,24 @@ | ||||
|     }, | ||||
|     "plugins": [ | ||||
|       "css-modules", | ||||
|       "jest", | ||||
|       "react", | ||||
|       "suggest-no-throw", | ||||
|       "@typescript-eslint" | ||||
|     ], | ||||
|     "extends": [ | ||||
|       "react-app", | ||||
|       "react-app/jest", | ||||
|       "plugin:css-modules/recommended" | ||||
|     ], | ||||
|     "rules": { | ||||
|       "@typescript-eslint/no-floating-promises": "error", | ||||
|       "@typescript-eslint/no-misused-promises": "error", | ||||
|       "no-restricted-globals": [ | ||||
|         "error", | ||||
|         { | ||||
|           "name": "isNaN", | ||||
|           "message": "Use Number.isNaN() instead." | ||||
|         } | ||||
|       ], | ||||
|       "semi": [ | ||||
|         "error", | ||||
|         "never" | ||||
|  | ||||
							
								
								
									
										14
									
								
								.github/ci-cd-scripts/playwright-electron.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -6,11 +6,11 @@ set -euo pipefail | ||||
| if [[ ! -f "test-results/.last-run.json" ]]; then | ||||
|     # if no last run artifact, than run plawright normally | ||||
|     echo "run playwright normally" | ||||
|         if [[ "$3" == ubuntu-latest* ]]; then | ||||
|         if [[ "$3" == *ubuntu* ]]; then | ||||
|             xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --shard=$1/$2 || true | ||||
|         elif [[ "$3" == windows* ]]; then | ||||
|         elif [[ "$3" == *windows* ]]; then | ||||
|             yarn test:playwright:electron:windows -- --shard=$1/$2 || true | ||||
|         elif [[ "$3" == macos-14* ]]; then | ||||
|         elif [[ "$3" == *macos* ]]; then | ||||
|             yarn test:playwright:electron:macos  -- --shard=$1/$2 || true | ||||
|         else | ||||
|             echo "Do not run playwright. Unable to detect os runtime." | ||||
| @ -21,7 +21,7 @@ if [[ ! -f "test-results/.last-run.json" ]]; then | ||||
| fi | ||||
|  | ||||
| retry=1 | ||||
| max_retrys=4 | ||||
| max_retrys=5 | ||||
|  | ||||
| # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues | ||||
| while [[ $retry -le $max_retrys ]]; do | ||||
| @ -30,11 +30,11 @@ while [[ $retry -le $max_retrys ]]; do | ||||
|         if [[ $failed_tests -gt 0 ]]; then | ||||
|             echo "retried=true" >>$GITHUB_OUTPUT | ||||
|             echo "run playwright with last failed tests and retry $retry" | ||||
|             if [[ "$3" == ubuntu-latest* ]]; then | ||||
|             if [[ "$3" == *ubuntu* ]]; then | ||||
|                 xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --last-failed || true | ||||
|             elif [[ "$3" == windows* ]]; then | ||||
|             elif [[ "$3" == *windows* ]]; then | ||||
|                 yarn test:playwright:electron:windows -- --last-failed || true | ||||
|             elif [[ "$3" == macos-14* ]]; then | ||||
|             elif [[ "$3" == *macos* ]]; then | ||||
|                 yarn test:playwright:electron:macos -- --last-failed || true | ||||
|             else | ||||
|                 echo "Do not run playwright. Unable to detect os runtime." | ||||
|  | ||||
							
								
								
									
										46
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -5,24 +5,28 @@ | ||||
|  | ||||
| version: 2 | ||||
| updates: | ||||
|     - package-ecosystem: 'npm' # See documentation for possible values | ||||
|       directory: '/' # Location of package manifests | ||||
|       schedule: | ||||
|           interval: 'weekly' | ||||
|       reviewers: | ||||
|           - franknoirot | ||||
|           - irev-dev | ||||
|     - package-ecosystem: 'github-actions' # See documentation for possible values | ||||
|       directory: '/' # Location of package manifests | ||||
|       schedule: | ||||
|           interval: 'weekly' | ||||
|       reviewers: | ||||
|           - adamchalmers | ||||
|           - jessfraz | ||||
|     - package-ecosystem: 'cargo' # See documentation for possible values | ||||
|       directory: '/src/wasm-lib/' # Location of package manifests | ||||
|       schedule: | ||||
|           interval: 'weekly' | ||||
|       reviewers: | ||||
|           - adamchalmers | ||||
|           - jessfraz | ||||
|   - package-ecosystem: 'npm' # See documentation for possible values | ||||
|     directory: '/' # Location of package manifests | ||||
|     schedule: | ||||
|       interval: 'weekly' | ||||
|     reviewers: | ||||
|       - franknoirot | ||||
|       - irev-dev | ||||
|   - package-ecosystem: 'github-actions' # See documentation for possible values | ||||
|     directory: '/' # Location of package manifests | ||||
|     schedule: | ||||
|       interval: 'weekly' | ||||
|     reviewers: | ||||
|       - adamchalmers | ||||
|       - jessfraz | ||||
|   - package-ecosystem: 'cargo' # See documentation for possible values | ||||
|     directory: '/src/wasm-lib/' # Location of package manifests | ||||
|     schedule: | ||||
|       interval: 'weekly' | ||||
|     reviewers: | ||||
|       - adamchalmers | ||||
|       - jessfraz | ||||
|     groups: | ||||
|       serde-dependencies: | ||||
|         patterns: | ||||
|           - "serde*" | ||||
|  | ||||
							
								
								
									
										16
									
								
								.github/workflows/build-apps.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -173,7 +173,13 @@ jobs: | ||||
|           CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | ||||
|           CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||||
|           WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} | ||||
|         run: yarn electron-builder --config --publish always | ||||
|           DEBUG: "electron-notarize*" | ||||
|         # TODO: Fix electron-notarize flakes. The logs above should help gather more data on failures | ||||
|         uses: nick-fields/retry@v3.0.0 | ||||
|         with: | ||||
|           timeout_minutes: 10 | ||||
|           max_attempts: 3 | ||||
|           command: yarn electron-builder --config --publish always | ||||
|  | ||||
|       - name: List artifacts in out/ | ||||
|         run: ls -R out | ||||
| @ -228,7 +234,13 @@ jobs: | ||||
|           CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | ||||
|           CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||||
|           WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} | ||||
|         run: yarn electron-builder --config --publish always | ||||
|           DEBUG: "electron-notarize*" | ||||
|         # TODO: Fix electron-notarize flakes. The logs above should help gather more data on failures | ||||
|         uses: nick-fields/retry@v3.0.0 | ||||
|         with: | ||||
|           timeout_minutes: 10 | ||||
|           max_attempts: 3 | ||||
|           command: yarn electron-builder --config --publish always | ||||
|  | ||||
|       - uses: actions/upload-artifact@v4 | ||||
|         if: ${{ env.IS_RELEASE == 'true' }} | ||||
|  | ||||
							
								
								
									
										22
									
								
								.github/workflows/cargo-test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -2,28 +2,8 @@ on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - 'src/wasm-lib/**.rs' | ||||
|       - 'src/wasm-lib/**.hbs' | ||||
|       - 'src/wasm-lib/**.gen' | ||||
|       - 'src/wasm-lib/**.snap' | ||||
|       - '**/Cargo.toml' | ||||
|       - '**/Cargo.lock' | ||||
|       - '**/rust-toolchain.toml' | ||||
|       - 'src/wasm-lib/**.kcl' | ||||
|       - .github/workflows/cargo-test.yml | ||||
|  | ||||
|   pull_request: | ||||
|     paths: | ||||
|       - 'src/wasm-lib/**.rs' | ||||
|       - 'src/wasm-lib/**.hbs' | ||||
|       - 'src/wasm-lib/**.gen' | ||||
|       - 'src/wasm-lib/**.snap' | ||||
|       - '**/Cargo.toml' | ||||
|       - '**/Cargo.lock' | ||||
|       - '**/rust-toolchain.toml' | ||||
|       - 'src/wasm-lib/**.kcl' | ||||
|       - .github/workflows/cargo-test.yml | ||||
|   workflow_dispatch: | ||||
| permissions: read-all | ||||
| concurrency: | ||||
| @ -71,7 +51,7 @@ jobs: | ||||
|           KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}} | ||||
|           RUST_MIN_STACK: 10485760000 | ||||
|       - name: Upload to codecov.io | ||||
|         uses: codecov/codecov-action@v4 | ||||
|         uses: codecov/codecov-action@v5 | ||||
|         with: | ||||
|           token: ${{secrets.CODECOV_TOKEN}} | ||||
|           fail_ci_if_error: true | ||||
|  | ||||
							
								
								
									
										123
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -1,13 +1,13 @@ | ||||
| name: E2E Tests | ||||
| on: | ||||
|   push: | ||||
|     branches: [ main, pierremtb/move-tests-to-electon ] | ||||
|     branches: [ main ] | ||||
|   pull_request: | ||||
|     branches: [ main ] | ||||
|  | ||||
| # concurrency: | ||||
| #   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | ||||
| #   cancel-in-progress: true | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| permissions: | ||||
|   contents: write | ||||
| @ -34,45 +34,36 @@ jobs: | ||||
|               - 'src/wasm-lib/**' | ||||
|  | ||||
|   electron: | ||||
|     timeout-minutes: 120 | ||||
|     timeout-minutes: 60 | ||||
|     name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         # os: [ubuntu-latest-8-cores, windows-16-cores, macos-14-large] | ||||
|         os: [ubuntu-latest-8-cores, macos-14-large] | ||||
|         # TODO: enable self-hosted-windows-8-cores once available | ||||
|         os: [namespace-profile-ubuntu-8-cores, namespace-profile-macos-8-cores, windows-16-cores] | ||||
|         shardIndex: [1, 2, 3, 4] | ||||
|         shardTotal: [4] | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     needs: check-rust-changes | ||||
|     steps: | ||||
|  | ||||
|     - name: Tune GitHub-hosted runner network | ||||
|       uses: smorimoto/tune-github-hosted-runner-network@v1 | ||||
|  | ||||
|     - uses: actions/checkout@v4 | ||||
|    | ||||
|     - uses: actions/setup-node@v4 | ||||
|       with: | ||||
|         node-version-file: '.nvmrc' | ||||
|         cache: 'yarn' | ||||
|  | ||||
|     - uses: KittyCAD/action-install-cli@main | ||||
|     - name: Install dependencies | ||||
|       shell: bash | ||||
|       run: yarn | ||||
|  | ||||
|     - name: Cache Playwright Browsers | ||||
|       uses: actions/cache@v4 | ||||
|       with: | ||||
|         path: | | ||||
|           ~/.cache/ms-playwright/ | ||||
|         key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }} | ||||
|    | ||||
|     - name: Install Playwright Browsers | ||||
|       shell: bash | ||||
|       run: yarn playwright install --with-deps | ||||
|   | ||||
|     - name: Download Wasm Cache | ||||
|       id: download-wasm | ||||
|       if: needs.check-rust-changes.outputs.rust-changed == 'false' | ||||
| @ -84,71 +75,64 @@ jobs: | ||||
|         workflow: build-and-store-wasm.yml | ||||
|         branch: main | ||||
|         path: src/wasm-lib/pkg | ||||
|  | ||||
|     - name: copy wasm blob | ||||
|       if: needs.check-rust-changes.outputs.rust-changed == 'false' | ||||
|       shell: bash | ||||
|       run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public | ||||
|       continue-on-error: true | ||||
|  | ||||
|     - name: Setup Rust | ||||
|       uses: dtolnay/rust-toolchain@stable | ||||
|  | ||||
|     # - name: Cache Wasm (because rust diff) | ||||
|     #   if: needs.check-rust-changes.outputs.rust-changed == 'true' | ||||
|     #   uses: Swatinem/rust-cache@v2 | ||||
|     #   with: | ||||
|     #     workspaces: './src/wasm-lib' | ||||
|  | ||||
|     # - name: OR Cache Wasm (because wasm cache failed) | ||||
|     #   if: steps.download-wasm.outcome == 'failure' | ||||
|     #   uses: Swatinem/rust-cache@v2 | ||||
|     #   with: | ||||
|     #     workspaces: './src/wasm-lib' | ||||
|  | ||||
|     - name: Cache Wasm (because rust diff) | ||||
|       if: needs.check-rust-changes.outputs.rust-changed == 'true' | ||||
|       uses: Swatinem/rust-cache@v2 | ||||
|       with: | ||||
|         workspaces: './src/wasm-lib' | ||||
|     - name: OR Cache Wasm (because wasm cache failed) | ||||
|       if: steps.download-wasm.outcome == 'failure' | ||||
|       uses: Swatinem/rust-cache@v2 | ||||
|       with: | ||||
|         workspaces: './src/wasm-lib' | ||||
|     - name: install good sed | ||||
|       if:  ${{ startsWith(matrix.os, 'macos') }} | ||||
|       shell: bash | ||||
|       run: | | ||||
|         brew install gnu-sed | ||||
|         echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH | ||||
|  | ||||
|     # - name: Install vector | ||||
|     #   shell: bash | ||||
|     #   if:  ${{ !startsWith(matrix.os, 'windows') }} | ||||
|     #   run: | | ||||
|     #     curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh | ||||
|     #     chmod +x /tmp/vector.sh | ||||
|     #     /tmp/vector.sh -y -no-modify-path | ||||
|     #     mkdir -p /tmp/vector | ||||
|     #     cp .github/workflows/vector.toml /tmp/vector.toml | ||||
|     #     sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml | ||||
|     #     sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml | ||||
|     #     sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml | ||||
|     #     sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml | ||||
|     #     sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml | ||||
|     #     cat /tmp/vector.toml | ||||
|     #     ${HOME}/.vector/bin/vector --config /tmp/vector.toml & | ||||
|  | ||||
|     - name: Install vector | ||||
|       shell: bash | ||||
|       # TODO: figure out what to do with this, it's failing | ||||
|       if: false | ||||
|       run: | | ||||
|         curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh | ||||
|         chmod +x /tmp/vector.sh | ||||
|         /tmp/vector.sh -y -no-modify-path | ||||
|         mkdir -p /tmp/vector | ||||
|         cp .github/workflows/vector.toml /tmp/vector.toml | ||||
|         sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml | ||||
|         sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml | ||||
|         sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml | ||||
|         sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml | ||||
|         sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml | ||||
|         cat /tmp/vector.toml | ||||
|         ${HOME}/.vector/bin/vector --config /tmp/vector.toml & | ||||
|     - name: Build Wasm (because rust diff) | ||||
|       # if: needs.check-rust-changes.outputs.rust-changed == 'true' | ||||
|       if: needs.check-rust-changes.outputs.rust-changed == 'true' | ||||
|       shell: bash | ||||
|       run: yarn build:wasm | ||||
|     - name: OR Build Wasm (because wasm cache failed) | ||||
|       if: steps.download-wasm.outcome == 'failure' | ||||
|       shell: bash | ||||
|       run: yarn build:wasm | ||||
|  | ||||
|     # - name: OR Build Wasm (because wasm cache failed) | ||||
|     #   if: steps.download-wasm.outcome == 'failure' | ||||
|     #   shell: bash | ||||
|     #   run: yarn build:wasm | ||||
|  | ||||
|     - name: build electron | ||||
|       shell: bash | ||||
|       run: yarn tron:package | ||||
|   | ||||
|     - name: Run ubuntu/chrome snapshots | ||||
|       if: ${{ startsWith(matrix.os, 'ubuntu') }} | ||||
|       if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }} | ||||
|       shell: bash | ||||
|       # TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest, | ||||
|       # but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes. | ||||
|       run: | | ||||
|         PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot  --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} | ||||
|         PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot  --shard=1/1 | ||||
|       env: | ||||
|         CI: true | ||||
|         NODE_ENV: development | ||||
| @ -156,23 +140,20 @@ jobs: | ||||
|         VITE_KC_SKIP_AUTH: true | ||||
|         token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} | ||||
|         snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }} | ||||
|  | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: ${{ startsWith(matrix.os, 'ubuntu') }} | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       with: | ||||
|         name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         path: playwright-report/ | ||||
|         include-hidden-files: true | ||||
|         retention-days: 30 | ||||
|         overwrite: true | ||||
|  | ||||
|     - name: Clean up test-results | ||||
|       if: ${{ startsWith(matrix.os, 'ubuntu') }} | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       continue-on-error: true | ||||
|       run: rm -r test-results | ||||
|  | ||||
|     - name: check for changes | ||||
|       if: ${{ startsWith(matrix.os, 'ubuntu') }} | ||||
|       if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }} | ||||
|       shell: bash | ||||
|       id: git-check | ||||
|       run: | | ||||
| @ -181,7 +162,6 @@ jobs: | ||||
|           then echo "modified=true" >> $GITHUB_OUTPUT | ||||
|           else echo "modified=false" >> $GITHUB_OUTPUT | ||||
|           fi | ||||
|  | ||||
|     - name: Commit changes, if any | ||||
|       if: steps.git-check.outputs.modified == 'true' | ||||
|       shell: bash | ||||
| @ -196,7 +176,6 @@ jobs: | ||||
|         git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true | ||||
|         git push | ||||
|         git push origin ${{ github.head_ref }} | ||||
|  | ||||
|     # only upload artifacts if there's actually changes | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: steps.git-check.outputs.modified == 'true' | ||||
| @ -205,16 +184,15 @@ jobs: | ||||
|         path: playwright-report/ | ||||
|         include-hidden-files: true | ||||
|         retention-days: 30 | ||||
|  | ||||
|     - uses: actions/download-artifact@v4 | ||||
|       if: ${{ startsWith(matrix.os, 'linux') }} | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       continue-on-error: true | ||||
|       with: | ||||
|         name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         path: test-results/ | ||||
|  | ||||
|     - name: Run playwright/electron flow (with retries) | ||||
|       id: retry | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       shell: bash | ||||
|       run: | | ||||
|         .github/ci-cd-scripts/playwright-electron.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{matrix.os}} | ||||
| @ -225,19 +203,20 @@ jobs: | ||||
|         VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} | ||||
|         VITE_KC_SKIP_AUTH: true | ||||
|         token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} | ||||
|  | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: always() | ||||
|       with: | ||||
|         name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         path: test-results/ | ||||
|         include-hidden-files: true | ||||
|         retention-days: 30 | ||||
|         overwrite: true | ||||
|  | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: always() | ||||
|       with: | ||||
|         name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         path: playwright-report/ | ||||
|         include-hidden-files: true | ||||
|         retention-days: 30 | ||||
|         overwrite: true | ||||
|  | ||||
|  | ||||
							
								
								
									
										40
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @ -337,13 +337,47 @@ For individual testing: | ||||
| yarn test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false | ||||
| ``` | ||||
|  | ||||
| Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default. | ||||
| Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro) tests, in interactive mode by default. | ||||
|  | ||||
| ### Rust tests | ||||
|  | ||||
| ```bash | ||||
| **Dependencies** | ||||
|  | ||||
| - `KITTYCAD_API_TOKEN` | ||||
| - `cargo-nextest` | ||||
| - `just` | ||||
|  | ||||
| #### Setting KITTYCAD_API_TOKEN | ||||
| Use the production zoo.dev token, set this environment variable before running the tests | ||||
|  | ||||
| #### Installing cargonextest | ||||
|  | ||||
| ``` | ||||
| cd src/wasm-lib | ||||
| KITTYCAD_API_TOKEN=XXX cargo test -- --test-threads=1 | ||||
| cargo search cargo-nextest | ||||
| cargo install cargo-nextest | ||||
| ``` | ||||
|  | ||||
| #### just | ||||
| install [`just`](https://github.com/casey/just?tab=readme-ov-file#pre-built-binaries) | ||||
|  | ||||
| #### Running the tests | ||||
|  | ||||
| ```bash | ||||
| # With just | ||||
| # Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set | ||||
| # Make sure you installed cargo-nextest | ||||
| # Make sure you installed just | ||||
| cd src/wasm-lib | ||||
| just test | ||||
| ``` | ||||
|  | ||||
| ```bash | ||||
| # Without just | ||||
| # Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set | ||||
| # Make sure you installed cargo-nextest | ||||
| cd src/wasm-lib | ||||
| export RUST_BRACKTRACE="full" && cargo nextest run --workspace --test-threads=1 | ||||
| ``` | ||||
|  | ||||
| Where `XXX` is an API token from the production engine (NOT the dev environment). | ||||
|  | ||||
| @ -24,3 +24,5 @@ once fixed in engine will just start working here with no language changes. | ||||
|     chamfer cases work currently. | ||||
|  | ||||
| - **Appearance**: Changing the appearance on a loft does not work. | ||||
|  | ||||
| - **Helix**: Currently sweeping a helix does not work. | ||||
|  | ||||
							
								
								
									
										49
									
								
								docs/kcl/atan2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										42
									
								
								docs/kcl/circleThreePoint.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										43
									
								
								docs/kcl/helixRevolutions.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -30,10 +30,12 @@ layout: manual | ||||
| * [`assertLessThan`](kcl/assertLessThan) | ||||
| * [`assertLessThanOrEq`](kcl/assertLessThanOrEq) | ||||
| * [`atan`](kcl/atan) | ||||
| * [`atan2`](kcl/atan2) | ||||
| * [`bezierCurve`](kcl/bezierCurve) | ||||
| * [`ceil`](kcl/ceil) | ||||
| * [`chamfer`](kcl/chamfer) | ||||
| * [`circle`](kcl/circle) | ||||
| * [`circleThreePoint`](kcl/circleThreePoint) | ||||
| * [`close`](kcl/close) | ||||
| * [`cm`](kcl/cm) | ||||
| * [`cos`](kcl/cos) | ||||
| @ -46,6 +48,7 @@ layout: manual | ||||
| * [`getOppositeEdge`](kcl/getOppositeEdge) | ||||
| * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) | ||||
| * [`helix`](kcl/helix) | ||||
| * [`helixRevolutions`](kcl/helixRevolutions) | ||||
| * [`hole`](kcl/hole) | ||||
| * [`hollow`](kcl/hollow) | ||||
| * [`import`](kcl/import) | ||||
| @ -79,6 +82,7 @@ layout: manual | ||||
| * [`pi`](kcl/pi) | ||||
| * [`polar`](kcl/polar) | ||||
| * [`polygon`](kcl/polygon) | ||||
| * [`pop`](kcl/pop) | ||||
| * [`pow`](kcl/pow) | ||||
| * [`profileStart`](kcl/profileStart) | ||||
| * [`profileStartX`](kcl/profileStartX) | ||||
| @ -100,8 +104,8 @@ layout: manual | ||||
| * [`sin`](kcl/sin) | ||||
| * [`sqrt`](kcl/sqrt) | ||||
| * [`startProfileAt`](kcl/startProfileAt) | ||||
| * [`startSketchAt`](kcl/startSketchAt) | ||||
| * [`startSketchOn`](kcl/startSketchOn) | ||||
| * [`sweep`](kcl/sweep) | ||||
| * [`tan`](kcl/tan) | ||||
| * [`tangentToEnd`](kcl/tangentToEnd) | ||||
| * [`tangentialArc`](kcl/tangentialArc) | ||||
|  | ||||
| @ -35,7 +35,7 @@ The transform function returns a transform object. All properties of the object | ||||
|    - `rotation.origin` (either "local" i.e. rotate around its own center, "global" i.e. rotate around the scene's center, or a 3D point, defaults to "local") | ||||
|  | ||||
| ```js | ||||
| patternTransform(total_instances: u32, transform_function: FunctionParam, solid_set: SolidSet) -> [Solid] | ||||
| patternTransform(total_instances: integer, transform_function: FunctionParam, solid_set: SolidSet) -> [Solid] | ||||
| ``` | ||||
|  | ||||
|  | ||||
| @ -43,7 +43,7 @@ patternTransform(total_instances: u32, transform_function: FunctionParam, solid_ | ||||
|  | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `total_instances` | `u32` |  | Yes | | ||||
| | `total_instances` | `integer` |  | Yes | | ||||
| | `transform_function` | `FunctionParam` |  | Yes | | ||||
| | `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | A solid or a group of solids. | Yes | | ||||
|  | ||||
| @ -95,7 +95,8 @@ fn cube(length, center) { | ||||
|   p2 = [l + x, l + y] | ||||
|   p3 = [l + x, -l + y] | ||||
|  | ||||
|   return startSketchAt(p0) | ||||
|   return startSketchOn('XY') | ||||
|     |> startProfileAt(p0, %) | ||||
|     |> lineTo(p1, %) | ||||
|     |> lineTo(p2, %) | ||||
|     |> lineTo(p3, %) | ||||
| @ -132,7 +133,8 @@ fn cube(length, center) { | ||||
|   p2 = [l + x, l + y] | ||||
|   p3 = [l + x, -l + y] | ||||
|  | ||||
|   return startSketchAt(p0) | ||||
|   return startSketchOn('XY') | ||||
|     |> startProfileAt(p0, %) | ||||
|     |> lineTo(p1, %) | ||||
|     |> lineTo(p2, %) | ||||
|     |> lineTo(p3, %) | ||||
| @ -195,7 +197,8 @@ fn transform(i) { | ||||
|     { rotation = { angle = 45 * i } } | ||||
|   ] | ||||
| } | ||||
| startSketchAt([0, 0]) | ||||
| startSketchOn('XY') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> polygon({ | ||||
|        radius = 10, | ||||
|        numSides = 4, | ||||
|  | ||||
| @ -9,7 +9,7 @@ Just like patternTransform, but works on 2D sketches not 3D solids. | ||||
|  | ||||
|  | ||||
| ```js | ||||
| patternTransform2d(total_instances: u32, transform_function: FunctionParam, solid_set: SketchSet) -> [Sketch] | ||||
| patternTransform2d(total_instances: integer, transform_function: FunctionParam, solid_set: SketchSet) -> [Sketch] | ||||
| ``` | ||||
|  | ||||
|  | ||||
| @ -17,7 +17,7 @@ patternTransform2d(total_instances: u32, transform_function: FunctionParam, soli | ||||
|  | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `total_instances` | `u32` |  | Yes | | ||||
| | `total_instances` | `integer` |  | Yes | | ||||
| | `transform_function` | `FunctionParam` |  | Yes | | ||||
| | `solid_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes | | ||||
|  | ||||
|  | ||||
							
								
								
									
										39
									
								
								docs/kcl/pop.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -43,7 +43,7 @@ fn sum(arr) { | ||||
|  | ||||
| /* The above is basically like this pseudo-code: | ||||
| fn sum(arr): | ||||
|     let sumSoFar = 0 | ||||
|     sumSoFar = 0 | ||||
|     for i in arr: | ||||
|         sumSoFar = add(sumSoFar, i) | ||||
|     return sumSoFar */ | ||||
| @ -79,10 +79,11 @@ fn decagon(radius) { | ||||
|   stepAngle = 1 / 10 * tau() | ||||
|  | ||||
|   // Start the decagon sketch at this point. | ||||
|   startOfDecagonSketch = startSketchAt([cos(0) * radius, sin(0) * radius]) | ||||
|   startOfDecagonSketch = startSketchOn('XY') | ||||
|     |> startProfileAt([cos(0) * radius, sin(0) * radius], %) | ||||
|  | ||||
|   // Use a `reduce` to draw the remaining decagon sides. | ||||
|   // For each number in the array 1..10, run the given function, | ||||
|     // Use a `reduce` to draw the remaining decagon sides. | ||||
|     // For each number in the array 1..10, run the given function, | ||||
|   // which takes a partially-sketched decagon and adds one more edge to it. | ||||
|   fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) { | ||||
|     // Draw one edge of the decagon. | ||||
| @ -96,14 +97,15 @@ fn decagon(radius) { | ||||
|  | ||||
| /* The `decagon` above is basically like this pseudo-code: | ||||
| fn decagon(radius): | ||||
|     let stepAngle = (1/10) * tau() | ||||
|     let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)]) | ||||
|     stepAngle = (1/10) * tau() | ||||
|     plane = startSketchOn('XY') | ||||
|     startOfDecagonSketch = startProfileAt([(cos(0)*radius), (sin(0) * radius)], plane) | ||||
|  | ||||
|     // Here's the reduce part. | ||||
|     let partialDecagon = startOfDecagonSketch | ||||
|     partialDecagon = startOfDecagonSketch | ||||
|     for i in [1..10]: | ||||
|         let x = cos(stepAngle * i) * radius | ||||
|         let y = sin(stepAngle * i) * radius | ||||
|         x = cos(stepAngle * i) * radius | ||||
|         y = sin(stepAngle * i) * radius | ||||
|         partialDecagon = lineTo([x, y], partialDecagon) | ||||
|     fullDecagon = partialDecagon // it's now full | ||||
|     return fullDecagon */ | ||||
|  | ||||
| @ -28,7 +28,8 @@ segEnd(tag: TagIdentifier) -> [number] | ||||
|  | ||||
| ```js | ||||
| w = 15 | ||||
| cube = startSketchAt([0, 0]) | ||||
| cube = startSketchOn('XY') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([w, 0], %, $line1) | ||||
|   |> line([0, w], %, $line2) | ||||
|   |> line([-w, 0], %, $line3) | ||||
| @ -37,7 +38,8 @@ cube = startSketchAt([0, 0]) | ||||
|   |> extrude(5, %) | ||||
|  | ||||
| fn cylinder(radius, tag) { | ||||
|   return startSketchAt([0, 0]) | ||||
|   return startSketchOn('XY') | ||||
|     |> startProfileAt([0, 0], %) | ||||
|     |> circle({ | ||||
|          radius = radius, | ||||
|          center = segEnd(tag) | ||||
|  | ||||
| @ -28,7 +28,8 @@ segStart(tag: TagIdentifier) -> [number] | ||||
|  | ||||
| ```js | ||||
| w = 15 | ||||
| cube = startSketchAt([0, 0]) | ||||
| cube = startSketchOn('XY') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([w, 0], %, $line1) | ||||
|   |> line([0, w], %, $line2) | ||||
|   |> line([-w, 0], %, $line3) | ||||
| @ -37,7 +38,8 @@ cube = startSketchAt([0, 0]) | ||||
|   |> extrude(5, %) | ||||
|  | ||||
| fn cylinder(radius, tag) { | ||||
|   return startSketchAt([0, 0]) | ||||
|   return startSketchOn('XY') | ||||
|     |> startProfileAt([0, 0], %) | ||||
|     |> circle({ | ||||
|          radius = radius, | ||||
|          center = segStart(tag) | ||||
|  | ||||
| @ -4,6 +4,8 @@ excerpt: "Start a new 2-dimensional sketch at a given point on the 'XY' plane." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| **WARNING:** This function is deprecated. | ||||
|  | ||||
| Start a new 2-dimensional sketch at a given point on the 'XY' plane. | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										19652
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										77
									
								
								docs/kcl/sweep.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -13,13 +13,18 @@ Data to draw an angled line. | ||||
|  | ||||
| An angle and length with explicitly named parameters | ||||
|  | ||||
| [`PolarCoordsData`](/docs/kcl/types/PolarCoordsData) | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `angle` |`number`| The angle of the line (in degrees). | No | | ||||
| | `length` |`number`| The length of the line. | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| @ -1,19 +1,19 @@ | ||||
| --- | ||||
| title: "AxisOrEdgeReference" | ||||
| excerpt: "Axis or tagged edge." | ||||
| title: "Axis2dOrEdgeReference" | ||||
| excerpt: "A 2D axis or tagged edge." | ||||
| layout: manual | ||||
| --- | ||||
| 
 | ||||
| Axis or tagged edge. | ||||
| A 2D axis or tagged edge. | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| **This schema accepts any of the following:** | ||||
| 
 | ||||
| Axis and origin. | ||||
| 2D axis and origin. | ||||
| 
 | ||||
| [`AxisAndOrigin`](/docs/kcl/types/AxisAndOrigin) | ||||
| [`AxisAndOrigin2d`](/docs/kcl/types/AxisAndOrigin2d) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										42
									
								
								docs/kcl/types/Axis3dOrEdgeReference.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,42 @@ | ||||
| --- | ||||
| title: "Axis3dOrEdgeReference" | ||||
| excerpt: "A 3D axis or tagged edge." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| A 3D axis or tagged edge. | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts any of the following:** | ||||
|  | ||||
| 3D axis and origin. | ||||
|  | ||||
| [`AxisAndOrigin3d`](/docs/kcl/types/AxisAndOrigin3d) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Tagged edge. | ||||
|  | ||||
| [`EdgeReference`](/docs/kcl/types/EdgeReference) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| --- | ||||
| title: "AxisAndOrigin" | ||||
| excerpt: "Axis and origin." | ||||
| title: "AxisAndOrigin2d" | ||||
| excerpt: "A 2D axis and origin." | ||||
| layout: manual | ||||
| --- | ||||
| 
 | ||||
| Axis and origin. | ||||
| A 2D axis and origin. | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										105
									
								
								docs/kcl/types/AxisAndOrigin3d.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,105 @@ | ||||
| --- | ||||
| title: "AxisAndOrigin3d" | ||||
| excerpt: "A 3D axis and origin." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| A 3D axis and origin. | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts exactly one of the following:** | ||||
|  | ||||
| X-axis. | ||||
|  | ||||
| **enum:** `X` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Y-axis. | ||||
|  | ||||
| **enum:** `Y` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Z-axis. | ||||
|  | ||||
| **enum:** `Z` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Flip the X-axis. | ||||
|  | ||||
| **enum:** `-X` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Flip the Y-axis. | ||||
|  | ||||
| **enum:** `-Y` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Flip the Z-axis. | ||||
|  | ||||
| **enum:** `-Z` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `custom` |`object`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										23
									
								
								docs/kcl/types/CircleThreePointData.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,23 @@ | ||||
| --- | ||||
| title: "CircleThreePointData" | ||||
| excerpt: "Data for drawing a 3-point circle" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Data for drawing a 3-point circle | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `p1` |`[number, number]`| Point one for circle derivation. | No | | ||||
| | `p2` |`[number, number]`| Point two for circle derivation. | No | | ||||
| | `p3` |`[number, number]`| Point three for circle derivation. | No | | ||||
|  | ||||
|  | ||||
							
								
								
									
										25
									
								
								docs/kcl/types/Helix.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,25 @@ | ||||
| --- | ||||
| title: "Helix" | ||||
| excerpt: "A helix." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| A helix. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `value` |`string`| The id of the helix. | No | | ||||
| | `revolutions` |`number`| Number of revolutions. | No | | ||||
| | `angleStart` |`number`| Start angle (in degrees). | No | | ||||
| | `ccw` |`boolean`| Is the helix rotation counter clockwise? | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| --- | ||||
| title: "HelixData" | ||||
| excerpt: "Data for helices." | ||||
| excerpt: "Data for a helix." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Data for helices. | ||||
| Data for a helix. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
| @ -19,6 +19,8 @@ Data for helices. | ||||
| | `revolutions` |`number`| Number of revolutions. | No | | ||||
| | `angleStart` |`number`| Start angle (in degrees). | No | | ||||
| | `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No | | ||||
| | `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No | | ||||
| | `length` |`number`| Length of the helix. | No | | ||||
| | `radius` |`number`| Radius of the helix. | No | | ||||
| | `axis` |[`Axis3dOrEdgeReference`](/docs/kcl/types/Axis3dOrEdgeReference)| Axis to use as mirror. | No | | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										24
									
								
								docs/kcl/types/HelixRevolutionsData.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,24 @@ | ||||
| --- | ||||
| title: "HelixRevolutionsData" | ||||
| excerpt: "Data for helix revolutions." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Data for helix revolutions. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `revolutions` |`number`| Number of revolutions. | No | | ||||
| | `angleStart` |`number`| Start angle (in degrees). | No | | ||||
| | `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No | | ||||
| | `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No | | ||||
|  | ||||
|  | ||||
							
								
								
									
										25
									
								
								docs/kcl/types/HelixValue.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,25 @@ | ||||
| --- | ||||
| title: "HelixValue" | ||||
| excerpt: "A helix." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| A helix. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `value` |`string`| The id of the helix. | No | | ||||
| | `revolutions` |`number`| Number of revolutions. | No | | ||||
| | `angleStart` |`number`| Start angle (in degrees). | No | | ||||
| | `ccw` |`boolean`| Is the helix rotation counter clockwise? | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
|  | ||||
| @ -12,5 +12,10 @@ KCL value for an optional parameter which was not given an argument. (remember, | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -285,6 +285,27 @@ An solid is a collection of extrude surfaces. | ||||
| | `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
| A helix. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: [`Helix`](/docs/kcl/types/Helix)|  | No | | ||||
| | `value` |`string`| The id of the helix. | No | | ||||
| | `revolutions` |`number`| Number of revolutions. | No | | ||||
| | `angleStart` |`number`| Start angle (in degrees). | No | | ||||
| | `ccw` |`boolean`| Is the helix rotation counter clockwise? | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Data for an imported geometry. | ||||
|  | ||||
| @ -329,6 +350,23 @@ Data for an imported geometry. | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `Module`|  | No | | ||||
| | `value` |[`ModuleId`](/docs/kcl/types/ModuleId)| Any KCL value. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
|  | ||||
| @ -16,6 +16,6 @@ Data for a mirror. | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis to use as mirror. | No | | ||||
| | `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis to use as mirror. | No | | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										16
									
								
								docs/kcl/types/ModuleId.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,16 @@ | ||||
| --- | ||||
| title: "ModuleId" | ||||
| excerpt: "Identifier of a source file.  Uses a u32 to keep the size small." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Identifier of a source file.  Uses a u32 to keep the size small. | ||||
|  | ||||
| **Type:** `integer` (`uint32`) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -17,7 +17,7 @@ Data for revolution surfaces. | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `angle` |`number` (**maximum:** 360.0) (**minimum:** -360.0)| Angle to revolve (in degrees). Default is 360. | No | | ||||
| | `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis of revolution. | No | | ||||
| | `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis of revolution. | No | | ||||
| | `tolerance` |`number`| Tolerance for the revolve operation. | No | | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										23
									
								
								docs/kcl/types/SweepData.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,23 @@ | ||||
| --- | ||||
| title: "SweepData" | ||||
| excerpt: "Data for a sweep." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Data for a sweep. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `path` |[`SweepPath`](/docs/kcl/types/SweepPath)| The path to sweep along. | No | | ||||
| | `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No | | ||||
| | `tolerance` |`number`| Tolerance for the sweep operation. | No | | ||||
|  | ||||
|  | ||||
							
								
								
									
										42
									
								
								docs/kcl/types/SweepPath.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,42 @@ | ||||
| --- | ||||
| title: "SweepPath" | ||||
| excerpt: "A path to sweep along." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| A path to sweep along. | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts any of the following:** | ||||
|  | ||||
| A path to sweep along. | ||||
|  | ||||
| [`Sketch`](/docs/kcl/types/Sketch) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| A path to sweep along. | ||||
|  | ||||
| [`Helix`](/docs/kcl/types/Helix) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -149,7 +149,7 @@ test.describe('Basic sketch', () => { | ||||
|     await doBasicSketch(page, homePage, ['code']) | ||||
|   }) | ||||
|  | ||||
|   test.fixme('code pane closed at start', async ({ page, homePage }) => { | ||||
|   test('code pane closed at start', async ({ page, homePage }) => { | ||||
|     // Load the app with the code panes | ||||
|     await page.addInitScript(async (persistModelingContext) => { | ||||
|       localStorage.setItem( | ||||
|  | ||||
| @ -45,7 +45,8 @@ test.describe('Command bar tests', () => { | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   test('Fillet from command bar', async ({ page, homePage }) => { | ||||
|   // TODO: fix this test after the electron migration | ||||
|   test.fixme('Fillet from command bar', async ({ page, homePage }) => { | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|  | ||||
| @ -54,6 +54,108 @@ test.describe('Editor tests', () => { | ||||
|   |> close(%)`) | ||||
|   }) | ||||
|  | ||||
|   test('ensure we use the cache, and do not re-execute', async ({ | ||||
|     homePage, | ||||
|     page, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.waitForPageLoad() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.type(`sketch001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|  | ||||
|     // Ensure we execute the first time. | ||||
|     await u.openDebugPanel() | ||||
|     await expect( | ||||
|       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||
|     ).toHaveCount(1) | ||||
|     await expect( | ||||
|       page.locator('[data-message-type="execution-done"]') | ||||
|     ).toHaveCount(2) | ||||
|  | ||||
|     // Add whitespace to the end of the code. | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.press('ArrowUp') | ||||
|     await page.keyboard.press('ArrowUp') | ||||
|     await page.keyboard.press('ArrowUp') | ||||
|     await page.keyboard.press('ArrowUp') | ||||
|     await page.keyboard.press('Home') | ||||
|     await page.keyboard.type('    ') | ||||
|     await page.keyboard.press('Enter') | ||||
|     await page.keyboard.type('    ') | ||||
|  | ||||
|     // Ensure we don't execute the second time. | ||||
|     await u.openDebugPanel() | ||||
|     // Make sure we didn't clear the scene. | ||||
|     await expect( | ||||
|       page.locator('[data-message-type="execution-done"]') | ||||
|     ).toHaveCount(3) | ||||
|     await expect( | ||||
|       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||
|     ).toHaveCount(1) | ||||
|   }) | ||||
|  | ||||
|   test('ensure we use the cache, and do not clear on append', async ({ | ||||
|     homePage, | ||||
|     page, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.waitForPageLoad() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.type(`sketch001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|  | ||||
|     // Ensure we execute the first time. | ||||
|     await u.openDebugPanel() | ||||
|     await expect( | ||||
|       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||
|     ).toHaveCount(1) | ||||
|     await expect( | ||||
|       page.locator('[data-message-type="execution-done"]') | ||||
|     ).toHaveCount(2) | ||||
|  | ||||
|     // Add whitespace to the end of the code. | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('End') | ||||
|     await page.keyboard.press('Enter') | ||||
|     await page.keyboard.press('Enter') | ||||
|     await page.keyboard.type('const x = 1') | ||||
|     await page.keyboard.press('Enter') | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await expect( | ||||
|       page.locator('[data-message-type="execution-done"]') | ||||
|     ).toHaveCount(3) | ||||
|     await expect( | ||||
|       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||
|     ).toHaveCount(1) | ||||
|   }) | ||||
|  | ||||
|   test('if you click the format button it formats your code', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
| @ -462,7 +564,7 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|     /* add the following code to the editor (~ error is not a valid line) | ||||
|       * the old check here used $ but this is for tags so it changed meaning. | ||||
|       * hopefully ~ doesnt change meaning | ||||
|       * hopefully ~ doesn't change meaning | ||||
|     ~ error | ||||
|     const topAng = 30 | ||||
|     const bottomAng = 25 | ||||
|  | ||||
							
								
								
									
										127
									
								
								e2e/playwright/feature-tree-pane.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,127 @@ | ||||
| import { test, expect } from './zoo-test' | ||||
| import * as fsp from 'fs/promises' | ||||
| import { join } from 'path' | ||||
|  | ||||
| const FEATURE_TREE_EXAMPLE_CODE = `export fn timesFive(x) { | ||||
|   return 5 * x | ||||
| } | ||||
| export fn triangle() { | ||||
|   return startSketchOn('XZ') | ||||
|     |> startProfileAt([0, 0], %) | ||||
|     |> xLine(10, %) | ||||
|     |> line([-10, -5], %) | ||||
|     |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|     |> close(%) | ||||
| } | ||||
|  | ||||
| length001 = timesFive(1) * 5 | ||||
| sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([20, 10], %) | ||||
|   |> line([10, 10], %) | ||||
|   |> angledLine([-45, length001], %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| revolve001 = revolve({ axis = "X" }, sketch001) | ||||
| triangle() | ||||
|   |> extrude(30, %) | ||||
| plane001 = offsetPlane('XY', 10) | ||||
| sketch002 = startSketchOn(plane001) | ||||
|   |> startProfileAt([-20, 0], %) | ||||
|   |> line([5, -15], %) | ||||
|   |> xLine(-10, %) | ||||
|   |> lineTo([-40, 0], %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(10, sketch002) | ||||
| ` | ||||
|  | ||||
| test.describe('Feature Tree pane', () => { | ||||
|   test( | ||||
|     'User can go to definition and go to function definition', | ||||
|     { tag: '@electron' }, | ||||
|     async ({ context, homePage, scene, editor, toolbar }) => { | ||||
|       await context.folderSetupFn(async (dir) => { | ||||
|         const bracketDir = join(dir, 'test-sample') | ||||
|         await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|         await fsp.writeFile( | ||||
|           join(bracketDir, 'main.kcl'), | ||||
|           FEATURE_TREE_EXAMPLE_CODE, | ||||
|           'utf-8' | ||||
|         ) | ||||
|       }) | ||||
|  | ||||
|       await test.step('setup test', async () => { | ||||
|         await homePage.expectState({ | ||||
|           projectCards: [ | ||||
|             { | ||||
|               title: 'test-sample', | ||||
|               fileCount: 1, | ||||
|             }, | ||||
|           ], | ||||
|           sortBy: 'last-modified-desc', | ||||
|         }) | ||||
|         await homePage.openProject('test-sample') | ||||
|         await scene.waitForExecutionDone() | ||||
|         await editor.closePane() | ||||
|         await toolbar.openFeatureTreePane() | ||||
|       }) | ||||
|  | ||||
|       async function testViewSource({ | ||||
|         operationName, | ||||
|         operationIndex, | ||||
|         expectedActiveLine, | ||||
|       }: { | ||||
|         operationName: string | ||||
|         operationIndex: number | ||||
|         expectedActiveLine: string | ||||
|       }) { | ||||
|         await test.step(`Go to definition of the ${operationName}`, async () => { | ||||
|           await toolbar.viewSourceOnOperation(operationName, operationIndex) | ||||
|           await editor.expectState({ | ||||
|             highlightedCode: '', | ||||
|             diagnostics: [], | ||||
|             activeLines: [expectedActiveLine], | ||||
|           }) | ||||
|           await expect( | ||||
|             editor.activeLine.first(), | ||||
|             `${operationName} code should be scrolled into view` | ||||
|           ).toBeVisible() | ||||
|         }) | ||||
|       } | ||||
|  | ||||
|       await testViewSource({ | ||||
|         operationName: 'Offset Plane', | ||||
|         operationIndex: 0, | ||||
|         expectedActiveLine: "plane001 = offsetPlane('XY', 10)", | ||||
|       }) | ||||
|       await testViewSource({ | ||||
|         operationName: 'Extrude', | ||||
|         operationIndex: 1, | ||||
|         expectedActiveLine: 'extrude001 = extrude(10, sketch002)', | ||||
|       }) | ||||
|       await testViewSource({ | ||||
|         operationName: 'Revolve', | ||||
|         operationIndex: 0, | ||||
|         expectedActiveLine: 'revolve001 = revolve({ axis = "X" }, sketch001)', | ||||
|       }) | ||||
|       await testViewSource({ | ||||
|         operationName: 'Triangle', | ||||
|         operationIndex: 0, | ||||
|         expectedActiveLine: 'triangle()', | ||||
|       }) | ||||
|  | ||||
|       await test.step('Go to definition on the triangle function', async () => { | ||||
|         await toolbar.goToDefinitionOnOperation('Triangle', 0) | ||||
|         await editor.expectState({ | ||||
|           highlightedCode: '', | ||||
|           diagnostics: [], | ||||
|           activeLines: ['export fn triangle() {'], | ||||
|         }) | ||||
|         await expect( | ||||
|           editor.activeLine.first(), | ||||
|           'Triangle function definition should be scrolled into view' | ||||
|         ).toBeVisible() | ||||
|       }) | ||||
|     } | ||||
|   ) | ||||
| }) | ||||
| @ -92,6 +92,8 @@ test.describe('when using the file tree to', () => { | ||||
|     `rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ page }, testInfo) => { | ||||
|       // TODO: fix this test on windows after the electron migration | ||||
|       test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|       const { panesOpen, pasteCodeInEditor, renameFile, editorTextMatches } = | ||||
|         await getUtils(page, test) | ||||
|  | ||||
| @ -134,6 +136,8 @@ test.describe('when using the file tree to', () => { | ||||
|     `create many new files of the same name, incrementing their names`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ page }, testInfo) => { | ||||
|       // TODO: fix this test on windows after the electron migration | ||||
|       test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|       const { panesOpen, createNewFile } = await getUtils(page, test) | ||||
|  | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
| @ -1014,6 +1018,8 @@ test.describe('Undo and redo do not keep history when navigating between files', | ||||
|     `open a file, change something, open a different file, hitting undo should do nothing`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ context, page }, testInfo) => { | ||||
|       // TODO: fix this test on windows after the electron migration | ||||
|       test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|       await context.folderSetupFn(async (dir) => { | ||||
|         const testDir = join(dir, 'testProject') | ||||
|         await fsp.mkdir(testDir, { recursive: true }) | ||||
| @ -1082,6 +1088,8 @@ test.describe('Undo and redo do not keep history when navigating between files', | ||||
|     { tag: '@electron' }, | ||||
|     // Skip on windows i think the keybindings are different for redo. | ||||
|     async ({ context, page }, testInfo) => { | ||||
|       // TODO: fix this test on windows after the electron migration | ||||
|       test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|       await context.folderSetupFn(async (dir) => { | ||||
|         const testDir = join(dir, 'testProject') | ||||
|         await fsp.mkdir(testDir, { recursive: true }) | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import type { Page } from '@playwright/test' | ||||
| import type { Page, Locator } from '@playwright/test' | ||||
| import { expect } from '@playwright/test' | ||||
|  | ||||
| type CmdBarSerialised = | ||||
| @ -26,9 +26,11 @@ type CmdBarSerialised = | ||||
|  | ||||
| export class CmdBarFixture { | ||||
|   public page: Page | ||||
|   cmdBarOpenBtn!: Locator | ||||
|  | ||||
|   constructor(page: Page) { | ||||
|     this.page = page | ||||
|     this.cmdBarOpenBtn = page.getByTestId('command-bar-open-button') | ||||
|   } | ||||
|   reConstruct = (page: Page) => { | ||||
|     this.page = page | ||||
| @ -116,4 +118,21 @@ export class CmdBarFixture { | ||||
|       await this.page.keyboard.press('Enter') | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   openCmdBar = async (selectCmd?: 'promptToEdit') => { | ||||
|     // TODO why does this button not work in electron tests? | ||||
|     // await this.cmdBarOpenBtn.click() | ||||
|     await this.page.keyboard.down('ControlOrMeta') | ||||
|     await this.page.keyboard.press('KeyK') | ||||
|     await this.page.keyboard.up('ControlOrMeta') | ||||
|     await expect(this.page.getByPlaceholder('Search commands')).toBeVisible() | ||||
|     if (selectCmd === 'promptToEdit') { | ||||
|       const promptEditCommand = this.page.getByText( | ||||
|         'Use Zoo AI to edit your kcl' | ||||
|       ) | ||||
|       await expect(promptEditCommand.first()).toBeVisible() | ||||
|       await promptEditCommand.first().scrollIntoViewIfNeeded() | ||||
|       await promptEditCommand.first().click() | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -20,7 +20,7 @@ export class EditorFixture { | ||||
|   private diagnosticsTooltip!: Locator | ||||
|   private diagnosticsGutterIcon!: Locator | ||||
|   private codeContent!: Locator | ||||
|   private activeLine!: Locator | ||||
|   public activeLine!: Locator | ||||
|  | ||||
|   constructor(page: Page) { | ||||
|     this.page = page | ||||
| @ -147,20 +147,28 @@ export class EditorFixture { | ||||
|   openPane() { | ||||
|     return openPane(this.page, this.paneButtonTestId) | ||||
|   } | ||||
|   scrollToText(text: string) { | ||||
|     return this.page.evaluate((scrollToText: string) => { | ||||
|       // editorManager is available on the window object. | ||||
|       // @ts-ignore | ||||
|       let index = editorManager._editorView.docView.view.state.doc | ||||
|         .toString() | ||||
|         .indexOf(scrollToText) | ||||
|       // @ts-ignore | ||||
|       editorManager._editorView.dispatch({ | ||||
|         selection: { | ||||
|           anchor: index, | ||||
|         }, | ||||
|         scrollIntoView: true, | ||||
|       }) | ||||
|     }, text) | ||||
|   scrollToText(text: string, placeCursor?: boolean) { | ||||
|     return this.page.evaluate( | ||||
|       (args: { text: string; placeCursor?: boolean }) => { | ||||
|         // error TS2339: Property 'docView' does not exist on type 'EditorView'. | ||||
|         // Except it does so :shrug: | ||||
|         // @ts-ignore | ||||
|         let index = window.editorManager._editorView?.docView.view.state.doc | ||||
|           .toString() | ||||
|           .indexOf(args.text) | ||||
|         window.editorManager._editorView?.focus() | ||||
|         window.editorManager._editorView?.dispatch({ | ||||
|           selection: window.EditorSelection.create([ | ||||
|             window.EditorSelection.cursor(index), | ||||
|           ]), | ||||
|           effects: [ | ||||
|             window.EditorView.scrollIntoView( | ||||
|               window.EditorSelection.range(index, index + 1) | ||||
|             ), | ||||
|           ], | ||||
|         }) | ||||
|       }, | ||||
|       { text, placeCursor } | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -36,7 +36,8 @@ type DragFromHandler = ( | ||||
|  | ||||
| export class SceneFixture { | ||||
|   public page: Page | ||||
|  | ||||
|   public streamWrapper!: Locator | ||||
|   public loadingIndicator!: Locator | ||||
|   private exeIndicator!: Locator | ||||
|  | ||||
|   constructor(page: Page) { | ||||
| @ -64,6 +65,8 @@ export class SceneFixture { | ||||
|     this.page = page | ||||
|  | ||||
|     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') | ||||
|     this.streamWrapper = page.getByTestId('stream') | ||||
|     this.loadingIndicator = this.streamWrapper.getByTestId('loading') | ||||
|   } | ||||
|  | ||||
|   makeMouseHelpers = ( | ||||
| @ -218,23 +221,7 @@ export class SceneFixture { | ||||
|     coords: { x: number; y: number }, | ||||
|     diff: number | ||||
|   ) => { | ||||
|     let finalValue = colour | ||||
|     await expect | ||||
|       .poll(async () => { | ||||
|         const pixel = (await getPixelRGBs(this.page)(coords, 1))[0] | ||||
|         if (!pixel) return null | ||||
|         finalValue = pixel | ||||
|         return pixel.every( | ||||
|           (channel, index) => Math.abs(channel - colour[index]) < diff | ||||
|         ) | ||||
|       }) | ||||
|       .toBeTruthy() | ||||
|       .catch((cause) => { | ||||
|         throw new Error( | ||||
|           `ExpectPixelColor: expecting ${colour} got ${finalValue}`, | ||||
|           { cause } | ||||
|         ) | ||||
|       }) | ||||
|     await expectPixelColor(this.page, colour, coords, diff) | ||||
|   } | ||||
|  | ||||
|   get gizmo() { | ||||
| @ -251,3 +238,28 @@ export class SceneFixture { | ||||
|     await buttonToTest.click() | ||||
|   } | ||||
| } | ||||
|  | ||||
| export async function expectPixelColor( | ||||
|   page: Page, | ||||
|   colour: [number, number, number], | ||||
|   coords: { x: number; y: number }, | ||||
|   diff: number | ||||
| ) { | ||||
|   let finalValue = colour | ||||
|   await expect | ||||
|     .poll(async () => { | ||||
|       const pixel = (await getPixelRGBs(page)(coords, 1))[0] | ||||
|       if (!pixel) return null | ||||
|       finalValue = pixel | ||||
|       return pixel.every( | ||||
|         (channel, index) => Math.abs(channel - colour[index]) < diff | ||||
|       ) | ||||
|     }) | ||||
|     .toBeTruthy() | ||||
|     .catch((cause) => { | ||||
|       throw new Error( | ||||
|         `ExpectPixelColor: expecting ${colour} got ${finalValue}`, | ||||
|         { cause } | ||||
|       ) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| @ -1,12 +1,20 @@ | ||||
| import type { Page, Locator } from '@playwright/test' | ||||
| import { expect } from '../zoo-test' | ||||
| import { doAndWaitForImageDiff } from '../test-utils' | ||||
| import { | ||||
|   checkIfPaneIsOpen, | ||||
|   closePane, | ||||
|   doAndWaitForImageDiff, | ||||
|   openPane, | ||||
| } from '../test-utils' | ||||
| import { SidebarType } from 'components/ModelingSidebar/ModelingPanes' | ||||
| import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants' | ||||
|  | ||||
| export class ToolbarFixture { | ||||
|   public page: Page | ||||
|  | ||||
|   extrudeButton!: Locator | ||||
|   loftButton!: Locator | ||||
|   sweepButton!: Locator | ||||
|   shellButton!: Locator | ||||
|   offsetPlaneButton!: Locator | ||||
|   startSketchBtn!: Locator | ||||
| @ -20,6 +28,10 @@ export class ToolbarFixture { | ||||
|   filePane!: Locator | ||||
|   exeIndicator!: Locator | ||||
|   treeInputField!: Locator | ||||
|   /** The sidebar button for the Feature Tree pane */ | ||||
|   featureTreeId = 'feature-tree' as const | ||||
|   /** The pane element for the Feature Tree */ | ||||
|   featureTreePane!: Locator | ||||
|  | ||||
|   constructor(page: Page) { | ||||
|     this.page = page | ||||
| @ -29,6 +41,7 @@ export class ToolbarFixture { | ||||
|     this.page = page | ||||
|     this.extrudeButton = page.getByTestId('extrude') | ||||
|     this.loftButton = page.getByTestId('loft') | ||||
|     this.sweepButton = page.getByTestId('sweep') | ||||
|     this.shellButton = page.getByTestId('shell') | ||||
|     this.offsetPlaneButton = page.getByTestId('plane-offset') | ||||
|     this.startSketchBtn = page.getByTestId('sketch') | ||||
| @ -41,6 +54,7 @@ export class ToolbarFixture { | ||||
|     this.treeInputField = page.getByTestId('tree-input-field') | ||||
|  | ||||
|     this.filePane = page.locator('#files-pane') | ||||
|     this.featureTreePane = page.locator('#feature-tree-pane') | ||||
|     this.fileCreateToast = page.getByText('Successfully created') | ||||
|     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') | ||||
|   } | ||||
| @ -91,4 +105,76 @@ export class ToolbarFixture { | ||||
|       await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async closePane(paneId: SidebarType) { | ||||
|     return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) | ||||
|   } | ||||
|   async openPane(paneId: SidebarType) { | ||||
|     return openPane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) | ||||
|   } | ||||
|   async checkIfPaneIsOpen(paneId: SidebarType) { | ||||
|     return checkIfPaneIsOpen(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) | ||||
|   } | ||||
|  | ||||
|   async openFeatureTreePane() { | ||||
|     return this.openPane(this.featureTreeId) | ||||
|   } | ||||
|   async closeFeatureTreePane() { | ||||
|     await this.closePane(this.featureTreeId) | ||||
|   } | ||||
|   async checkIfFeatureTreePaneIsOpen() { | ||||
|     return this.checkIfPaneIsOpen(this.featureTreeId) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get a specific operation button from the Feature Tree pane | ||||
|    */ | ||||
|   async getFeatureTreeOperation(operationName: string, operationIndex: number) { | ||||
|     await this.openFeatureTreePane() | ||||
|     await expect(this.featureTreePane).toBeVisible() | ||||
|     return this.featureTreePane | ||||
|       .getByRole('button', { | ||||
|         name: operationName, | ||||
|       }) | ||||
|       .nth(operationIndex) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * View source on a specific operation in the Feature Tree pane. | ||||
|    * @param operationName The name of the operation type | ||||
|    * @param operationIndex The index out of operations of this type | ||||
|    */ | ||||
|   async viewSourceOnOperation(operationName: string, operationIndex: number) { | ||||
|     const operationButton = await this.getFeatureTreeOperation( | ||||
|       operationName, | ||||
|       operationIndex | ||||
|     ) | ||||
|     const viewSourceMenuButton = this.page.getByRole('button', { | ||||
|       name: 'View KCL source code', | ||||
|     }) | ||||
|  | ||||
|     await operationButton.click({ button: 'right' }) | ||||
|     await expect(viewSourceMenuButton).toBeVisible() | ||||
|     await viewSourceMenuButton.click() | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Go to definition on a specific operation in the Feature Tree pane | ||||
|    */ | ||||
|   async goToDefinitionOnOperation( | ||||
|     operationName: string, | ||||
|     operationIndex: number | ||||
|   ) { | ||||
|     const operationButton = await this.getFeatureTreeOperation( | ||||
|       operationName, | ||||
|       operationIndex | ||||
|     ) | ||||
|     const goToDefinitionMenuButton = this.page.getByRole('button', { | ||||
|       name: 'View function definition', | ||||
|     }) | ||||
|  | ||||
|     await operationButton.click({ button: 'right' }) | ||||
|     await expect(goToDefinitionMenuButton).toBeVisible() | ||||
|     await goToDefinitionMenuButton.click() | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { | ||||
|   TEST_SETTINGS_ONBOARDING_USER_MENU, | ||||
| } from './storageStates' | ||||
| import * as TOML from '@iarna/toml' | ||||
| import { expectPixelColor } from './fixtures/sceneFixture' | ||||
|  | ||||
| // Because onboarding relies on an app setting we need to set it as incompletel | ||||
| // for all these tests. | ||||
| @ -27,6 +28,7 @@ test.describe('Onboarding tests', () => { | ||||
|       cleanProjectDir: true, | ||||
|     }, | ||||
|     async ({ context, page, homePage }) => { | ||||
|       const u = await getUtils(page) | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
| @ -35,10 +37,23 @@ test.describe('Onboarding tests', () => { | ||||
|         page.getByText('Welcome to Modeling App! This') | ||||
|       ).toBeVisible() | ||||
|  | ||||
|       // Test that the onboarding pane loaded | ||||
|       await expect( | ||||
|         page.getByText('Welcome to Modeling App! This') | ||||
|       ).toBeVisible() | ||||
|  | ||||
|       // *and* that the code is shown in the editor | ||||
|       await expect(page.locator('.cm-content')).toContainText( | ||||
|         '// Shelf Bracket' | ||||
|       ) | ||||
|  | ||||
|       // Make sure the model loaded | ||||
|       const XYPlanePoint = { x: 774, y: 116 } as const | ||||
|       const modelColor: [number, number, number] = [45, 45, 45] | ||||
|       await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) | ||||
|       expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan( | ||||
|         8 | ||||
|       ) | ||||
|     } | ||||
|   ) | ||||
|  | ||||
| @ -76,6 +91,14 @@ test.describe('Onboarding tests', () => { | ||||
|         await expect(page.locator('.cm-content')).toContainText( | ||||
|           '// Shelf Bracket' | ||||
|         ) | ||||
|  | ||||
|         // TODO: jess make less shit | ||||
|         // Make sure the model loaded | ||||
|         //const XYPlanePoint = { x: 986, y: 522 } as const | ||||
|         //const modelColor: [number, number, number] = [76, 76, 76] | ||||
|         //await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) | ||||
|  | ||||
|         //await expectPixelColor(page, modelColor, XYPlanePoint, 8) | ||||
|       }) | ||||
|     } | ||||
|   ) | ||||
| @ -125,7 +148,7 @@ test.describe('Onboarding tests', () => { | ||||
|       ) | ||||
|  | ||||
|       // There used to be old code here that checked if we stored the reset | ||||
|       // code into localStorage but that isnt the case on desktop. It gets | ||||
|       // code into localStorage but that isn't the case on desktop. It gets | ||||
|       // saved to the file system, which we have other tests for. | ||||
|     } | ||||
|   ) | ||||
| @ -414,7 +437,7 @@ test.describe('Onboarding tests', () => { | ||||
|   ) | ||||
| }) | ||||
|  | ||||
| test( | ||||
| test.fixme( | ||||
|   'Restarting onboarding on desktop takes one attempt', | ||||
|   { | ||||
|     appSettings: { | ||||
| @ -486,7 +509,15 @@ test( | ||||
|     await test.step('Confirm that the onboarding has restarted', async () => { | ||||
|       await expect(tutorialProjectIndicator).toBeVisible() | ||||
|       await expect(tutorialModalText).toBeVisible() | ||||
|       // Make sure the model loaded | ||||
|       const XYPlanePoint = { x: 988, y: 523 } as const | ||||
|       const modelColor: [number, number, number] = [76, 76, 76] | ||||
|  | ||||
|       await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) | ||||
|       await expectPixelColor(page, modelColor, XYPlanePoint, 8) | ||||
|       await tutorialDismissButton.click() | ||||
|       // Make sure model still there. | ||||
|       await expectPixelColor(page, modelColor, XYPlanePoint, 8) | ||||
|     }) | ||||
|  | ||||
|     await test.step('Clear code and restart onboarding from settings', async () => { | ||||
|  | ||||
| @ -16,6 +16,8 @@ test('verify extruding circle works', async ({ | ||||
|   toolbar, | ||||
|   scene, | ||||
| }) => { | ||||
|   // TODO: fix this test on windows after the electron migration | ||||
|   test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|   const file = await fs.readFile( | ||||
|     path.resolve( | ||||
|       __dirname, | ||||
| @ -95,6 +97,8 @@ test('verify extruding circle works', async ({ | ||||
| }) | ||||
|  | ||||
| test.describe('verify sketch on chamfer works', () => { | ||||
|   // TODO: fix this test on windows after the electron migration | ||||
|   test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|   const _sketchOnAChamfer = | ||||
|     ( | ||||
|       page: Page, | ||||
| @ -752,6 +756,17 @@ test(`Offset plane point-and-click`, async ({ | ||||
|     }) | ||||
|     await scene.expectPixelColor([74, 74, 74], testPoint, 15) | ||||
|   }) | ||||
|  | ||||
|   await test.step('Delete offset plane via feature tree selection', async () => { | ||||
|     await editor.closePane() | ||||
|     const operationButton = await toolbar.getFeatureTreeOperation( | ||||
|       'Offset Plane', | ||||
|       0 | ||||
|     ) | ||||
|     await operationButton.click({ button: 'left' }) | ||||
|     await page.keyboard.press('Backspace') | ||||
|     await scene.expectPixelColor([50, 51, 96], testPoint, 15) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| const loftPointAndClickCases = [ | ||||
| @ -847,6 +862,173 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => { | ||||
|       }) | ||||
|       await scene.expectPixelColor([89, 89, 89], testPoint, 15) | ||||
|     }) | ||||
|  | ||||
|     await test.step('Delete loft via feature tree selection', async () => { | ||||
|       await editor.closePane() | ||||
|       const operationButton = await toolbar.getFeatureTreeOperation('Loft', 0) | ||||
|       await operationButton.click({ button: 'left' }) | ||||
|       await page.keyboard.press('Backspace') | ||||
|       await scene.expectPixelColor([254, 254, 254], testPoint, 15) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| // TODO: merge with above test. Right now we're not able to delete a loft | ||||
| // right after creation via selection for some reason, so we go with a new instance | ||||
| test('Loft and offset plane deletion via selection', async ({ | ||||
|   context, | ||||
|   page, | ||||
|   homePage, | ||||
|   scene, | ||||
| }) => { | ||||
|   const initialCode = `sketch001 = startSketchOn('XZ') | ||||
|   |> circle({ center = [0, 0], radius = 30 }, %) | ||||
|   plane001 = offsetPlane('XZ', 50) | ||||
|   sketch002 = startSketchOn(plane001) | ||||
|   |> circle({ center = [0, 0], radius = 20 }, %) | ||||
| loft001 = loft([sketch001, sketch002]) | ||||
| ` | ||||
|   await context.addInitScript((initialCode) => { | ||||
|     localStorage.setItem('persistCode', initialCode) | ||||
|   }, initialCode) | ||||
|   await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|   await homePage.goToModelingScene() | ||||
|  | ||||
|   // One dumb hardcoded screen pixel value | ||||
|   const testPoint = { x: 575, y: 200 } | ||||
|   const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y) | ||||
|   const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 80) | ||||
|  | ||||
|   await test.step(`Delete loft`, async () => { | ||||
|     // Check for loft | ||||
|     await scene.expectPixelColor([89, 89, 89], testPoint, 15) | ||||
|     await clickOnSketch1() | ||||
|     await expect(page.locator('.cm-activeLine')).toHaveText(` | ||||
|       |> circle({ center = [0, 0], radius = 30 }, %) | ||||
|     `) | ||||
|     await page.keyboard.press('Backspace') | ||||
|     // Check for sketch 1 | ||||
|     await scene.expectPixelColor([254, 254, 254], testPoint, 15) | ||||
|   }) | ||||
|  | ||||
|   await test.step('Delete sketch002', async () => { | ||||
|     await page.waitForTimeout(1000) | ||||
|     await clickOnSketch2() | ||||
|     await expect(page.locator('.cm-activeLine')).toHaveText(` | ||||
|       |> circle({ center = [0, 0], radius = 20 }, %) | ||||
|     `) | ||||
|     await page.keyboard.press('Backspace') | ||||
|     // Check for plane001 | ||||
|     await scene.expectPixelColor([228, 228, 228], testPoint, 15) | ||||
|   }) | ||||
|  | ||||
|   await test.step('Delete plane001', async () => { | ||||
|     await page.waitForTimeout(1000) | ||||
|     await clickOnSketch2() | ||||
|     await expect(page.locator('.cm-activeLine')).toHaveText(` | ||||
|       plane001 = offsetPlane('XZ', 50) | ||||
|     `) | ||||
|     await page.keyboard.press('Backspace') | ||||
|     // Check for sketch 1 | ||||
|     await scene.expectPixelColor([254, 254, 254], testPoint, 15) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test(`Sweep point-and-click`, async ({ | ||||
|   context, | ||||
|   page, | ||||
|   homePage, | ||||
|   scene, | ||||
|   editor, | ||||
|   toolbar, | ||||
|   cmdBar, | ||||
| }) => { | ||||
|   const initialCode = `sketch001 = startSketchOn('YZ') | ||||
|   |> circle({ | ||||
|        center = [0, 0], | ||||
|        radius = 500 | ||||
|      }, %) | ||||
| sketch002 = startSketchOn('XZ') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> xLine(-500, %) | ||||
|   |> tangentialArcTo([-2000, 500], %) | ||||
| ` | ||||
|   await context.addInitScript((initialCode) => { | ||||
|     localStorage.setItem('persistCode', initialCode) | ||||
|   }, initialCode) | ||||
|   await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|   await homePage.goToModelingScene() | ||||
|   await scene.waitForExecutionDone() | ||||
|  | ||||
|   // One dumb hardcoded screen pixel value | ||||
|   const testPoint = { x: 700, y: 250 } | ||||
|   const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y) | ||||
|   const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y) | ||||
|   const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)' | ||||
|  | ||||
|   await test.step(`Look for sketch001`, async () => { | ||||
|     await toolbar.closePane('code') | ||||
|     await scene.expectPixelColor([53, 53, 53], testPoint, 15) | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Go through the command bar flow`, async () => { | ||||
|     await toolbar.sweepButton.click() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Sweep', | ||||
|       currentArgKey: 'profile', | ||||
|       currentArgValue: '', | ||||
|       headerArguments: { | ||||
|         Path: '', | ||||
|         Profile: '', | ||||
|       }, | ||||
|       highlightedHeaderArg: 'profile', | ||||
|       stage: 'arguments', | ||||
|     }) | ||||
|     await clickOnSketch1() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Sweep', | ||||
|       currentArgKey: 'path', | ||||
|       currentArgValue: '', | ||||
|       headerArguments: { | ||||
|         Path: '', | ||||
|         Profile: '1 face', | ||||
|       }, | ||||
|       highlightedHeaderArg: 'path', | ||||
|       stage: 'arguments', | ||||
|     }) | ||||
|     await clickOnSketch2() | ||||
|     await cmdBar.expectState({ | ||||
|       commandName: 'Sweep', | ||||
|       headerArguments: { | ||||
|         Path: '1 face', | ||||
|         Profile: '1 face', | ||||
|       }, | ||||
|       stage: 'review', | ||||
|     }) | ||||
|     await cmdBar.progressCmdBar() | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Confirm code is added to the editor, scene has changed`, async () => { | ||||
|     await scene.expectPixelColor([135, 64, 73], testPoint, 15) | ||||
|     await toolbar.openPane('code') | ||||
|     await editor.expectEditor.toContain(sweepDeclaration) | ||||
|     await editor.expectState({ | ||||
|       diagnostics: [], | ||||
|       activeLines: [sweepDeclaration], | ||||
|       highlightedCode: '', | ||||
|     }) | ||||
|     await toolbar.closePane('code') | ||||
|   }) | ||||
|  | ||||
|   await test.step('Delete sweep via feature tree selection', async () => { | ||||
|     await toolbar.openPane('feature-tree') | ||||
|     await page.waitForTimeout(500) | ||||
|     const operationButton = await toolbar.getFeatureTreeOperation('Sweep', 0) | ||||
|     await operationButton.click({ button: 'left' }) | ||||
|     await page.keyboard.press('Backspace') | ||||
|     await page.waitForTimeout(500) | ||||
|     await toolbar.closePane('feature-tree') | ||||
|     await scene.expectPixelColor([53, 53, 53], testPoint, 15) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -864,6 +1046,8 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { | ||||
|     toolbar, | ||||
|     cmdBar, | ||||
|   }) => { | ||||
|     // TODO: fix this test on windows after the electron migration | ||||
|     test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|     const initialCode = `sketch001 = startSketchOn('XZ') | ||||
|     |> circle({ center = [0, 0], radius = 30 }, %) | ||||
|     extrude001 = extrude(30, sketch001) | ||||
| @ -1024,4 +1208,104 @@ extrude001 = extrude(40, sketch001) | ||||
|     }) | ||||
|     await scene.expectPixelColor([49, 49, 49], testPoint, 15) | ||||
|   }) | ||||
|  | ||||
|   await test.step('Delete shell via feature tree selection', async () => { | ||||
|     await editor.closePane() | ||||
|     const operationButton = await toolbar.getFeatureTreeOperation('Shell', 0) | ||||
|     await operationButton.click({ button: 'left' }) | ||||
|     await page.keyboard.press('Backspace') | ||||
|     await scene.expectPixelColor([99, 99, 99], testPoint, 15) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| const shellSketchOnFacesCases = [ | ||||
|   `sketch001 = startSketchOn('XZ') | ||||
|   |> circle({ center = [0, 0], radius = 100 }, %) | ||||
|   |> extrude(100, %) | ||||
|  | ||||
| sketch002 = startSketchOn(sketch001, 'END') | ||||
|   |> circle({ center = [0, 0], radius = 50 }, %) | ||||
|   |> extrude(50, %) | ||||
|   `, | ||||
|   `sketch001 = startSketchOn('XZ') | ||||
|   |> circle({ center = [0, 0], radius = 100 }, %) | ||||
| extrude001 = extrude(100, sketch001) | ||||
|  | ||||
| sketch002 = startSketchOn(extrude001, 'END') | ||||
|   |> circle({ center = [0, 0], radius = 50 }, %) | ||||
| extrude002 = extrude(50, sketch002) | ||||
|   `, | ||||
| ] | ||||
| shellSketchOnFacesCases.forEach((initialCode, index) => { | ||||
|   const hasExtrudesInPipe = index === 0 | ||||
|   test(`Shell point-and-click sketch on face (extrudes in pipes: ${hasExtrudesInPipe})`, async ({ | ||||
|     context, | ||||
|     page, | ||||
|     homePage, | ||||
|     scene, | ||||
|     editor, | ||||
|     toolbar, | ||||
|     cmdBar, | ||||
|   }) => { | ||||
|     await context.addInitScript((initialCode) => { | ||||
|       localStorage.setItem('persistCode', initialCode) | ||||
|     }, initialCode) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|     await scene.waitForExecutionDone() | ||||
|  | ||||
|     // One dumb hardcoded screen pixel value | ||||
|     const testPoint = { x: 550, y: 295 } | ||||
|     const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y) | ||||
|     const shellDeclaration = `shell001 = shell({ faces = ['end'], thickness = 5 }, ${ | ||||
|       hasExtrudesInPipe ? 'sketch002' : 'extrude002' | ||||
|     })` | ||||
|  | ||||
|     await test.step(`Look for the grey of the shape`, async () => { | ||||
|       await toolbar.closePane('code') | ||||
|       await scene.expectPixelColor([128, 128, 128], testPoint, 15) | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Go through the command bar flow, selecting a cap and keeping default thickness`, async () => { | ||||
|       await toolbar.shellButton.click() | ||||
|       await cmdBar.expectState({ | ||||
|         stage: 'arguments', | ||||
|         currentArgKey: 'selection', | ||||
|         currentArgValue: '', | ||||
|         headerArguments: { | ||||
|           Selection: '', | ||||
|           Thickness: '', | ||||
|         }, | ||||
|         highlightedHeaderArg: 'selection', | ||||
|         commandName: 'Shell', | ||||
|       }) | ||||
|       await clickOnCap() | ||||
|       await page.waitForTimeout(500) | ||||
|       await cmdBar.progressCmdBar() | ||||
|       await page.waitForTimeout(500) | ||||
|       await cmdBar.progressCmdBar() | ||||
|       await page.waitForTimeout(500) | ||||
|       await cmdBar.expectState({ | ||||
|         stage: 'review', | ||||
|         headerArguments: { | ||||
|           Selection: '1 cap', | ||||
|           Thickness: '5', | ||||
|         }, | ||||
|         commandName: 'Shell', | ||||
|       }) | ||||
|       await cmdBar.progressCmdBar() | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Confirm code is added to the editor, scene has changed`, async () => { | ||||
|       await toolbar.openPane('code') | ||||
|       await editor.expectEditor.toContain(shellDeclaration) | ||||
|       await editor.expectState({ | ||||
|         diagnostics: [], | ||||
|         activeLines: [shellDeclaration], | ||||
|         highlightedCode: '', | ||||
|       }) | ||||
|       await toolbar.closePane('code') | ||||
|       await scene.expectPixelColor([73, 73, 73], testPoint, 15) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -115,7 +115,7 @@ test( | ||||
| ) | ||||
|  | ||||
| test( | ||||
|   'yyyyyyyyy 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' }, | ||||
|   async ({ context, page }, testInfo) => { | ||||
|     await context.folderSetupFn(async (dir) => { | ||||
| @ -199,7 +199,7 @@ test( | ||||
| ) | ||||
|  | ||||
| test( | ||||
|   'aaayyyyyyyy 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' }, | ||||
|   async ({ context, page }, testInfo) => { | ||||
|     await context.folderSetupFn(async (dir) => { | ||||
| @ -276,7 +276,7 @@ test( | ||||
| ) | ||||
|  | ||||
| test( | ||||
|   'nooooooooooooo open a file in a project works and renders, open empty file, it should clear the scene', | ||||
|   'open a file in a project works and renders, open empty file, it should clear the scene', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ context, page }, testInfo) => { | ||||
|     await context.folderSetupFn(async (dir) => { | ||||
| @ -1885,3 +1885,48 @@ test.fixme( | ||||
|     }) | ||||
|   } | ||||
| ) | ||||
|  | ||||
| test( | ||||
|   'project name with foreign characters should open', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ context, page }, testInfo) => { | ||||
|     await context.folderSetupFn(async (dir) => { | ||||
|       const bracketDir = path.join(dir, 'اَلْعَرَبِيَّةُ') | ||||
|       await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|       await fsp.copyFile( | ||||
|         executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|         path.join(bracketDir, 'main.kcl') | ||||
|       ) | ||||
|  | ||||
|       await fsp.writeFile(path.join(bracketDir, 'empty.kcl'), '') | ||||
|     }) | ||||
|  | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|     page.on('console', console.log) | ||||
|  | ||||
|     const pointOnModel = { x: 630, y: 280 } | ||||
|  | ||||
|     await test.step('Opening the اَلْعَرَبِيَّةُ project should load the stream', async () => { | ||||
|       // expect to see the text bracket | ||||
|       await expect(page.getByText('اَلْعَرَبِيَّةُ')).toBeVisible() | ||||
|  | ||||
|       await page.getByText('اَلْعَرَبِيَّةُ').click() | ||||
|  | ||||
|       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) | ||||
|     }) | ||||
|   } | ||||
| ) | ||||
|  | ||||
							
								
								
									
										190
									
								
								e2e/playwright/prompt-to-edit.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,190 @@ | ||||
| import { test, expect } from './zoo-test' | ||||
|  | ||||
| /* eslint-disable jest/no-conditional-expect */ | ||||
|  | ||||
| const file = `sketch001 = startSketchOn('XZ') | ||||
| profile001 = startProfileAt([57.81, 250.51], sketch001) | ||||
|   |> line([121.13, 56.63], %, $seg02) | ||||
|   |> line([83.37, -34.61], %, $seg01) | ||||
|   |> line([19.66, -116.4], %) | ||||
|   |> line([-221.8, -41.69], %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(200, profile001) | ||||
| sketch002 = startSketchOn('XZ') | ||||
|   |> startProfileAt([-73.64, -42.89], %) | ||||
|   |> xLine(173.71, %) | ||||
|   |> line([-22.12, -94.4], %) | ||||
|   |> xLine(-156.98, %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| extrude002 = extrude(50, sketch002) | ||||
| sketch003 = startSketchOn('XY') | ||||
|   |> startProfileAt([52.92, 157.81], %) | ||||
|   |> angledLine([0, 176.4], %, $rectangleSegmentA001) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001) - 90, | ||||
|        53.4 | ||||
|      ], %, $rectangleSegmentB001) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001), | ||||
|        -segLen(rectangleSegmentA001) | ||||
|      ], %, $rectangleSegmentC001) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| extrude003 = extrude(20, sketch003) | ||||
| ` | ||||
|  | ||||
| test.describe('Check the happy path, for basic changing color', () => { | ||||
|   const cases = [ | ||||
|     { | ||||
|       desc: 'User accepts change', | ||||
|       shouldReject: false, | ||||
|     }, | ||||
|     { | ||||
|       desc: 'User rejects change', | ||||
|       shouldReject: true, | ||||
|     }, | ||||
|   ] as const | ||||
|   for (const { desc, shouldReject } of cases) { | ||||
|     test(`${desc}`, async ({ | ||||
|       context, | ||||
|       homePage, | ||||
|       cmdBar, | ||||
|       editor, | ||||
|       page, | ||||
|       scene, | ||||
|     }) => { | ||||
|       await context.addInitScript((file) => { | ||||
|         localStorage.setItem('persistCode', file) | ||||
|       }, file) | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|       const body1CapCoords = { x: 571, y: 351 } | ||||
|       const greenCheckCoords = { x: 565, y: 345 } | ||||
|       const body2WallCoords = { x: 609, y: 153 } | ||||
|       const [clickBody1Cap] = scene.makeMouseHelpers( | ||||
|         body1CapCoords.x, | ||||
|         body1CapCoords.y | ||||
|       ) | ||||
|       const yellow: [number, number, number] = [179, 179, 131] | ||||
|       const green: [number, number, number] = [108, 152, 75] | ||||
|       const notGreen: [number, number, number] = [132, 132, 132] | ||||
|       const body2NotGreen: [number, number, number] = [88, 88, 88] | ||||
|       const submittingToast = page.getByText('Submitting to Text-to-CAD API...') | ||||
|       const successToast = page.getByText('Prompt to edit successful') | ||||
|       const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' }) | ||||
|       const rejectBtn = page.getByRole('button', { name: 'close Reject' }) | ||||
|  | ||||
|       await test.step('wait for scene to load select body and check selection came through', async () => { | ||||
|         await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15) | ||||
|         await clickBody1Cap() | ||||
|         await scene.expectPixelColor(yellow, body1CapCoords, 20) | ||||
|         await editor.expectState({ | ||||
|           highlightedCode: '', | ||||
|           activeLines: ['|>startProfileAt([-73.64,-42.89],%)'], | ||||
|           diagnostics: [], | ||||
|         }) | ||||
|       }) | ||||
|  | ||||
|       await test.step('fire off edit prompt', async () => { | ||||
|         await cmdBar.openCmdBar('promptToEdit') | ||||
|         // being specific about the color with a hex means asserting pixel color is more stable | ||||
|         await page | ||||
|           .getByTestId('cmd-bar-arg-value') | ||||
|           .fill('make this neon green please, use #39FF14') | ||||
|         await page.waitForTimeout(100) | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await expect(submittingToast).toBeVisible() | ||||
|         await expect(submittingToast).not.toBeVisible({ timeout: 2 * 60_000 }) // can take a while | ||||
|         await expect(successToast).toBeVisible() | ||||
|       }) | ||||
|  | ||||
|       await test.step('verify initial change', async () => { | ||||
|         await scene.expectPixelColor(green, greenCheckCoords, 15) | ||||
|         await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15) | ||||
|         await editor.expectEditor.toContain('appearance({') | ||||
|       }) | ||||
|  | ||||
|       if (!shouldReject) { | ||||
|         await test.step('check accept works and can be "undo"ed', async () => { | ||||
|           await acceptBtn.click() | ||||
|           await expect(successToast).not.toBeVisible() | ||||
|  | ||||
|           await scene.expectPixelColor(green, greenCheckCoords, 15) | ||||
|           await editor.expectEditor.toContain('appearance({') | ||||
|  | ||||
|           // ctrl-z works after accepting | ||||
|           await page.keyboard.down('ControlOrMeta') | ||||
|           await page.keyboard.press('KeyZ') | ||||
|           await page.keyboard.up('ControlOrMeta') | ||||
|           await editor.expectEditor.not.toContain('appearance({') | ||||
|           await scene.expectPixelColor(notGreen, greenCheckCoords, 15) | ||||
|         }) | ||||
|       } else { | ||||
|         await test.step('check reject works', async () => { | ||||
|           await rejectBtn.click() | ||||
|           await expect(successToast).not.toBeVisible() | ||||
|  | ||||
|           await scene.expectPixelColor(notGreen, greenCheckCoords, 15) | ||||
|           await editor.expectEditor.not.toContain('appearance({') | ||||
|         }) | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| }) | ||||
|  | ||||
| test.describe('bad path', () => { | ||||
|   test(`bad edit prompt`, async ({ | ||||
|     context, | ||||
|     homePage, | ||||
|     cmdBar, | ||||
|     editor, | ||||
|     toolbar, | ||||
|     page, | ||||
|     scene, | ||||
|   }) => { | ||||
|     await context.addInitScript((file) => { | ||||
|       localStorage.setItem('persistCode', file) | ||||
|     }, file) | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     const body1CapCoords = { x: 571, y: 351 } | ||||
|     const [clickBody1Cap] = scene.makeMouseHelpers( | ||||
|       body1CapCoords.x, | ||||
|       body1CapCoords.y | ||||
|     ) | ||||
|     const yellow: [number, number, number] = [179, 179, 131] | ||||
|     const submittingToast = page.getByText('Submitting to Text-to-CAD API...') | ||||
|     const failToast = page.getByText( | ||||
|       'Failed to edit your KCL code, please try again with a different prompt or selection' | ||||
|     ) | ||||
|  | ||||
|     await test.step('wait for scene to load and select body', async () => { | ||||
|       await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15) | ||||
|  | ||||
|       await clickBody1Cap() | ||||
|       await scene.expectPixelColor(yellow, body1CapCoords, 20) | ||||
|  | ||||
|       await editor.expectState({ | ||||
|         highlightedCode: '', | ||||
|         activeLines: ['|>startProfileAt([-73.64,-42.89],%)'], | ||||
|         diagnostics: [], | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     await test.step('fire of bad prompt', async () => { | ||||
|       await cmdBar.openCmdBar('promptToEdit') | ||||
|       await page | ||||
|         .getByTestId('cmd-bar-arg-value') | ||||
|         .fill('ansheusha asnthuatshoeuhtaoetuhthaeu laughs in dvorak') | ||||
|       await page.waitForTimeout(100) | ||||
|       await cmdBar.progressCmdBar() | ||||
|       await expect(submittingToast).toBeVisible() | ||||
|     }) | ||||
|     await test.step('check fail toast appeared', async () => { | ||||
|       await expect(submittingToast).not.toBeVisible({ timeout: 2 * 60_000 }) // can take a while | ||||
|       await expect(failToast).toBeVisible() | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
| @ -560,6 +560,8 @@ extrude001 = extrude(50, sketch001) | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     // TODO: fix this test on windows after the electron migration | ||||
|     test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|     // Constants and locators | ||||
| @ -604,7 +606,7 @@ extrude001 = extrude(50, sketch001) | ||||
|       } | ||||
|  | ||||
|       expect(middlePixelIsBackgroundColor, { | ||||
|         message: 'We no longer the default planes', | ||||
|         message: 'We should not see the default planes', | ||||
|       }).toBeTruthy() | ||||
|     }) | ||||
|  | ||||
| @ -612,6 +614,38 @@ extrude001 = extrude(50, sketch001) | ||||
|       await expect(gizmo).toBeVisible() | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   test(`Refreshing the app doesn't cause the stream to pause on long-executing files`, async ({ | ||||
|     context, | ||||
|     homePage, | ||||
|     scene, | ||||
|     toolbar, | ||||
|     viewport, | ||||
|   }) => { | ||||
|     await context.folderSetupFn(async (dir) => { | ||||
|       const legoDir = path.join(dir, 'lego') | ||||
|       await fsp.mkdir(legoDir, { recursive: true }) | ||||
|       await fsp.copyFile( | ||||
|         executorInputPath('lego.kcl'), | ||||
|         path.join(legoDir, 'main.kcl') | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Test setup`, async () => { | ||||
|       await homePage.openProject('lego') | ||||
|       await toolbar.closePane('code') | ||||
|     }) | ||||
|     await test.step(`Waiting for the loading spinner to disappear`, async () => { | ||||
|       await scene.loadingIndicator.waitFor({ state: 'detached' }) | ||||
|     }) | ||||
|     await test.step(`The part should start loading quickly, not waiting until execution is complete`, async () => { | ||||
|       await scene.expectPixelColor( | ||||
|         [143, 143, 143], | ||||
|         { x: (viewport?.width ?? 1200) / 2, y: (viewport?.height ?? 500) / 2 }, | ||||
|         15 | ||||
|       ) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| async function clickExportButton(page: Page) { | ||||
|  | ||||
| @ -39,8 +39,8 @@ test.describe('Sketch tests', () => { | ||||
|   ${startProfileAt1} | ||||
|   |> arc({ | ||||
|         radius = screwRadius, | ||||
|         angle_start = 0, | ||||
|         angle_end = 360 | ||||
|         angleStart = 0, | ||||
|         angleEnd = 360 | ||||
|       }, %) | ||||
|    | ||||
|     part001 = startSketchOn('XY') | ||||
| @ -60,8 +60,8 @@ test.describe('Sketch tests', () => { | ||||
|   |> yLine(wireOffset, %) | ||||
|   |> arc({ | ||||
|         radius = wireRadius, | ||||
|         angle_start = 0, | ||||
|         angle_end = 180 | ||||
|         angleStart = 0, | ||||
|         angleEnd = 180 | ||||
|       }, %) | ||||
|   |> yLine(-wireOffset, %) | ||||
|   |> xLine(-width / 4, %) | ||||
| @ -82,19 +82,16 @@ test.describe('Sketch tests', () => { | ||||
|     await u.closeDebugPanel() | ||||
|  | ||||
|     await page.getByText(selectionsSnippets.startProfileAt1).click() | ||||
|     await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Edit Sketch' }) | ||||
|     ).toBeVisible() | ||||
|  | ||||
|     await page.getByText(selectionsSnippets.startProfileAt2).click() | ||||
|     await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Edit Sketch' }) | ||||
|     ).toBeVisible() | ||||
|  | ||||
|     await page.getByText(selectionsSnippets.startProfileAt3).click() | ||||
|     await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Edit Sketch' }) | ||||
|     ).toBeVisible() | ||||
| @ -637,6 +634,8 @@ test.describe('Sketch tests', () => { | ||||
|   |> revolve({ axis = "X" }, %)`) | ||||
|   }) | ||||
|   test('Can add multiple sketches', async ({ page, homePage }) => { | ||||
|     // TODO: fix this test on windows after the electron migration | ||||
|     test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|     const viewportSize = { width: 1200, height: 500 } | ||||
| @ -834,6 +833,8 @@ test.describe('Sketch tests', () => { | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     // TODO: fix this test on windows after the electron migration | ||||
|     test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|     // this was a regression https://github.com/KittyCAD/modeling-app/issues/2832 | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
| @ -949,107 +950,108 @@ test.describe('Sketch tests', () => { | ||||
|   `.replace(/\s/g, '') | ||||
|     ) | ||||
|   }) | ||||
|   test('empty-scene default-planes act as expected', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     /** | ||||
|      * Tests the following things | ||||
|      * 1) The the planes are there on load because the scene is empty | ||||
|      * 2) The planes don't changes color when hovered initially | ||||
|      * 3) Putting something in the scene makes the planes hidden | ||||
|      * 4) Removing everything from the scene shows the plans again | ||||
|      * 3) Once "start sketch" is click, the planes do respond to hovers | ||||
|      * 4) Selecting a plan works as expected, i.e. sketch mode | ||||
|      * 5) Reloading the scene with something already in the scene means the planes are hidden | ||||
|      */ | ||||
|   // TODO: fix after electron migration is merged | ||||
|   test.fixme( | ||||
|     'empty-scene default-planes act as expected', | ||||
|     async ({ page, homePage }) => { | ||||
|       /** | ||||
|        * Tests the following things | ||||
|        * 1) The the planes are there on load because the scene is empty | ||||
|        * 2) The planes don't changes color when hovered initially | ||||
|        * 3) Putting something in the scene makes the planes hidden | ||||
|        * 4) Removing everything from the scene shows the plans again | ||||
|        * 3) Once "start sketch" is click, the planes do respond to hovers | ||||
|        * 4) Selecting a plan works as expected, i.e. sketch mode | ||||
|        * 5) Reloading the scene with something already in the scene means the planes are hidden | ||||
|        */ | ||||
|  | ||||
|     const u = await getUtils(page) | ||||
|     await homePage.goToModelingScene() | ||||
|       const u = await getUtils(page) | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.closeDebugPanel() | ||||
|       await u.openDebugPanel() | ||||
|       await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|       await u.closeDebugPanel() | ||||
|  | ||||
|     const XYPlanePoint = { x: 774, y: 116 } as const | ||||
|     const unHoveredColor: [number, number, number] = [47, 47, 93] | ||||
|     expect( | ||||
|       await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor) | ||||
|     ).toBeLessThan(8) | ||||
|       const XYPlanePoint = { x: 774, y: 116 } as const | ||||
|       const unHoveredColor: [number, number, number] = [47, 47, 93] | ||||
|       expect( | ||||
|         await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor) | ||||
|       ).toBeLessThan(8) | ||||
|  | ||||
|     await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) | ||||
|     await page.waitForTimeout(200) | ||||
|       await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) | ||||
|       await page.waitForTimeout(200) | ||||
|  | ||||
|     // color should not change for having been hovered | ||||
|     expect( | ||||
|       await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor) | ||||
|     ).toBeLessThan(8) | ||||
|       // color should not change for having been hovered | ||||
|       expect( | ||||
|         await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor) | ||||
|       ).toBeLessThan(8) | ||||
|  | ||||
|     await u.openAndClearDebugPanel() | ||||
|       await u.openAndClearDebugPanel() | ||||
|  | ||||
|     await u.codeLocator.fill(`sketch001 = startSketchOn('XY') | ||||
|       await u.codeLocator.fill(`sketch001 = startSketchOn('XY') | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> xLine(-20, %) | ||||
|   `) | ||||
|  | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|       await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|  | ||||
|     const noPlanesColor: [number, number, number] = [30, 30, 30] | ||||
|     expect( | ||||
|       await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor) | ||||
|     ).toBeLessThan(3) | ||||
|       const noPlanesColor: [number, number, number] = [30, 30, 30] | ||||
|       expect( | ||||
|         await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor) | ||||
|       ).toBeLessThan(3) | ||||
|  | ||||
|     await u.clearCommandLogs() | ||||
|     await u.removeCurrentCode() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|       await u.clearCommandLogs() | ||||
|       await u.removeCurrentCode() | ||||
|       await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|  | ||||
|     await expect | ||||
|       .poll(() => u.getGreatestPixDiff(XYPlanePoint, unHoveredColor), { | ||||
|         timeout: 5_000, | ||||
|       }) | ||||
|       .toBeLessThan(8) | ||||
|       await expect | ||||
|         .poll(() => u.getGreatestPixDiff(XYPlanePoint, unHoveredColor), { | ||||
|           timeout: 5_000, | ||||
|         }) | ||||
|         .toBeLessThan(8) | ||||
|  | ||||
|     // click start Sketch | ||||
|     await page.getByRole('button', { name: 'Start Sketch' }).click() | ||||
|     await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y, { steps: 50 }) | ||||
|     const hoveredColor: [number, number, number] = [93, 93, 127] | ||||
|     // now that we're expecting the user to select a plan, it does respond to hover | ||||
|     await expect | ||||
|       .poll(() => u.getGreatestPixDiff(XYPlanePoint, hoveredColor)) | ||||
|       .toBeLessThan(8) | ||||
|       // click start Sketch | ||||
|       await page.getByRole('button', { name: 'Start Sketch' }).click() | ||||
|       await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y, { steps: 50 }) | ||||
|       const hoveredColor: [number, number, number] = [93, 93, 127] | ||||
|       // now that we're expecting the user to select a plan, it does respond to hover | ||||
|       await expect | ||||
|         .poll(() => u.getGreatestPixDiff(XYPlanePoint, hoveredColor)) | ||||
|         .toBeLessThan(8) | ||||
|  | ||||
|     await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y) | ||||
|     await page.waitForTimeout(600) | ||||
|       await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y) | ||||
|       await page.waitForTimeout(600) | ||||
|  | ||||
|     await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y) | ||||
|     await page.waitForTimeout(200) | ||||
|     await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50) | ||||
|     await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|       await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y) | ||||
|       await page.waitForTimeout(200) | ||||
|       await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50) | ||||
|       await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt([11.8, 9.09], %) | ||||
|     |> line([3.39, -3.39], %) | ||||
|   `) | ||||
|  | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch001 = startSketchOn('XZ') | ||||
|       await page.addInitScript(async () => { | ||||
|         localStorage.setItem( | ||||
|           'persistCode', | ||||
|           `sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt([11.8, 9.09], %) | ||||
|     |> line([3.39, -3.39], %) | ||||
|   ` | ||||
|       ) | ||||
|     }) | ||||
|         ) | ||||
|       }) | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.closeDebugPanel() | ||||
|       await u.openDebugPanel() | ||||
|       await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|       await u.closeDebugPanel() | ||||
|  | ||||
|     // expect there to be no planes on load since there's something in the scene | ||||
|     expect( | ||||
|       await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor) | ||||
|     ).toBeLessThan(3) | ||||
|   }) | ||||
|       // expect there to be no planes on load since there's something in the scene | ||||
|       expect( | ||||
|         await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor) | ||||
|       ).toBeLessThan(3) | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test('Can attempt to sketch on revolved face', async ({ page, homePage }) => { | ||||
|     const u = await getUtils(page) | ||||
| @ -1321,3 +1323,85 @@ test.describe(`Sketching with offset planes`, () => { | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| // Regression test for https://github.com/KittyCAD/modeling-app/issues/4891 | ||||
| test.describe(`Click based selection don't brick the app when clicked out of range after format using cache`, () => { | ||||
|   test(`Can select a line that reformmed after entering sketch mode`, async ({ | ||||
|     context, | ||||
|     page, | ||||
|     scene, | ||||
|     toolbar, | ||||
|     editor, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     // We seed the scene with a single offset plane | ||||
|     await context.addInitScript(() => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([3.14, 3.14], %) | ||||
|   |> arcTo({ | ||||
|   end = [4, 2], | ||||
|   interior = [1, 2] | ||||
|   }, %) | ||||
| ` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await homePage.goToModelingScene() | ||||
|     await scene.waitForExecutionDone() | ||||
|  | ||||
|     await test.step(`format the code`, async () => { | ||||
|       // doesn't contain condensed version | ||||
|       await editor.expectEditor.not.toContain( | ||||
|         `arcTo({ end = [4, 2], interior = [1, 2] }, %)` | ||||
|       ) | ||||
|       // click the code to enter sketch mode | ||||
|       await page.getByText(`arcTo`).click() | ||||
|       // Format the code. | ||||
|       await page.locator('#code-pane button:first-child').click() | ||||
|       await page.locator('button:has-text("Format code")').click() | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Ensure the code reformatted`, async () => { | ||||
|       await editor.expectEditor.toContain( | ||||
|         `arcTo({ end = [4, 2], interior = [1, 2] }, %)` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     const [arcClick, arcHover] = scene.makeMouseHelpers(699, 337) | ||||
|     await test.step('Ensure we can hover the arc', async () => { | ||||
|       await arcHover() | ||||
|  | ||||
|       // Check that the code is highlighted | ||||
|       await editor.expectState({ | ||||
|         activeLines: ["sketch001=startSketchOn('XZ')"], | ||||
|         diagnostics: [], | ||||
|         highlightedCode: 'arcTo({end = [4, 2], interior = [1, 2]}, %)', | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     await test.step('reset the selection', async () => { | ||||
|       // Move the mouse out of the way | ||||
|       await page.mouse.move(655, 337) | ||||
|  | ||||
|       await editor.expectState({ | ||||
|         activeLines: ["sketch001=startSketchOn('XZ')"], | ||||
|         diagnostics: [], | ||||
|         highlightedCode: '', | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     await test.step('Ensure we can click the arc', async () => { | ||||
|       await arcClick() | ||||
|  | ||||
|       // Check that the code is highlighted | ||||
|       await editor.expectState({ | ||||
|         activeLines: [], | ||||
|         diagnostics: [], | ||||
|         highlightedCode: 'arcTo({end = [4, 2], interior = [1, 2]}, %)', | ||||
|       }) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -375,6 +375,7 @@ const extrudeDefaultPlane = async (context: any, page: any, plane: string) => { | ||||
|   await u.closeKclCodePanel() | ||||
|   await expect(page).toHaveScreenshot({ | ||||
|     maxDiffPixels: 100, | ||||
|     mask: [page.getByTestId('model-state-indicator')], | ||||
|   }) | ||||
|   await u.openKclCodePanel() | ||||
| } | ||||
| @ -1168,3 +1169,109 @@ test.fixme('theme persists', async ({ page, context }) => { | ||||
|     maxDiffPixels: 100, | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test.describe('code color goober', { tag: '@snapshot' }, () => { | ||||
|   test('code color goober', async ({ page, context }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await context.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `// Create a pipe using a sweep. | ||||
|  | ||||
| // Create a path for the sweep. | ||||
| sweepPath = startSketchOn('XZ') | ||||
|   |> startProfileAt([0.05, 0.05], %) | ||||
|   |> line([0, 7], %) | ||||
|   |> tangentialArc({ offset = 90, radius = 5 }, %) | ||||
|   |> line([-3, 0], %) | ||||
|   |> tangentialArc({ offset = -90, radius = 5 }, %) | ||||
|   |> line([0, 7], %) | ||||
|  | ||||
| sweepSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([2, 0], %) | ||||
|   |> arc({ | ||||
|        angleEnd = 360, | ||||
|        angleStart = 0, | ||||
|        radius = 2 | ||||
|      }, %) | ||||
|   |> sweep({ | ||||
|     path = sweepPath, | ||||
|   }, %) | ||||
|   |> appearance({ | ||||
|        color = "#bb00ff", | ||||
|        metalness = 90, | ||||
|        roughness = 90 | ||||
|      }, %) | ||||
| ` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 1000 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.clearAndCloseDebugPanel() | ||||
|  | ||||
|     await expect(page, 'expect small color widget').toHaveScreenshot({ | ||||
|       maxDiffPixels: 100, | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   test('code color goober opening window', async ({ page, context }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await context.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `// Create a pipe using a sweep. | ||||
|  | ||||
| // Create a path for the sweep. | ||||
| sweepPath = startSketchOn('XZ') | ||||
|   |> startProfileAt([0.05, 0.05], %) | ||||
|   |> line([0, 7], %) | ||||
|   |> tangentialArc({ offset = 90, radius = 5 }, %) | ||||
|   |> line([-3, 0], %) | ||||
|   |> tangentialArc({ offset = -90, radius = 5 }, %) | ||||
|   |> line([0, 7], %) | ||||
|  | ||||
| sweepSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([2, 0], %) | ||||
|   |> arc({ | ||||
|        angleEnd = 360, | ||||
|        angleStart = 0, | ||||
|        radius = 2 | ||||
|      }, %) | ||||
|   |> sweep({ | ||||
|     path = sweepPath, | ||||
|   }, %) | ||||
|   |> appearance({ | ||||
|        color = "#bb00ff", | ||||
|        metalness = 90, | ||||
|        roughness = 90 | ||||
|      }, %) | ||||
| ` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 1000 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.clearAndCloseDebugPanel() | ||||
|  | ||||
|     await expect(page.locator('.cm-css-color-picker-wrapper')).toBeVisible() | ||||
|  | ||||
|     // Click the color widget | ||||
|     await page.locator('.cm-css-color-picker-wrapper input').click() | ||||
|  | ||||
|     await expect( | ||||
|       page, | ||||
|       'expect small color widget to have window open' | ||||
|     ).toHaveScreenshot({ | ||||
|       maxDiffPixels: 100, | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 49 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 52 KiB | 
| Before Width: | Height: | Size: 46 KiB | 
| Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB | 
| Before Width: | Height: | Size: 55 KiB | 
| Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 53 KiB | 
| Before Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 41 KiB | 
| Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB | 
| Before Width: | Height: | Size: 39 KiB | 
| Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB | 
| Before Width: | Height: | Size: 34 KiB | 
| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 42 KiB | 
| Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB | 
| Before Width: | Height: | Size: 51 KiB | 
| Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB | 
| Before Width: | Height: | Size: 65 KiB | 
| Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB | 
| Before Width: | Height: | Size: 62 KiB | 
| After Width: | Height: | Size: 144 KiB | 
| After Width: | Height: | Size: 128 KiB | 
| Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 37 KiB | 
| Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 53 KiB | 
| Before Width: | Height: | Size: 40 KiB | 
| Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 39 KiB | 
| Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 52 KiB | 
| Before Width: | Height: | Size: 39 KiB | 
| Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 53 KiB | 
| Before Width: | Height: | Size: 42 KiB | 
