Compare commits
	
		
			149 Commits
		
	
	
		
			nightly-v2
			...
			pierremtb/
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fecf5c2ee7 | |||
| 8ef31a0be1 | |||
| 3adb42b5f2 | |||
| 20016b101e | |||
| 8d9dbf36c3 | |||
| 440704ed9f | |||
| 2261217a5d | |||
| 10da986649 | |||
| 10789d9c3c | |||
| 67cc4f5835 | |||
| 2692f2b73a | |||
| 965cb18059 | |||
| a022b8ef6c | |||
| 4d24bf7c94 | |||
| 9a537da183 | |||
| df81b76b8b | |||
| ac3f7ab712 | |||
| d531728675 | |||
| 1d78fc15ac | |||
| c32aebc8ad | |||
| 997ebce3eb | |||
| dac91d3b79 | |||
| 1eaf371b44 | |||
| 0698432abf | |||
| 54da18d8ab | |||
| 2fe5ef7034 | |||
| 16b5eeadb1 | |||
| 7be4001839 | |||
| ffb2559787 | |||
| 0592d3b5da | |||
| 31e4d60045 | |||
| c0817b00e4 | |||
| 4ea1d16fb6 | |||
| d049bf33e8 | |||
| 7b11047d07 | |||
| 412e9568f2 | |||
| 9be208e5e1 | |||
| 842ef5ede9 | |||
| 3f855d7bad | |||
| 0a1a6e50cf | |||
| d4e955289c | |||
| c147a219f4 | |||
| 38513a1e25 | |||
| c0c5c790ca | |||
| 8b60f75220 | |||
| f91ad4331f | |||
| 59103a2118 | |||
| 9737c2550a | |||
| bf9d01a8dd | |||
| 702e322f90 | |||
| e82830754d | |||
| 7806377a5a | |||
| 859afa2fd8 | |||
| 0a5f3093fc | |||
| b65f7939f6 | |||
| c35dea5e07 | |||
| fc66d4745f | |||
| b313d26c2a | |||
| 00b94ead62 | |||
| 0531ea1ce9 | |||
| 5f9a4887c1 | |||
| da7dfa16d8 | |||
| 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 | 
| @ -1,3 +1,3 @@ | |||||||
| [codespell] | [codespell] | ||||||
| ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall | ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser | ||||||
| skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts | 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 | ||||||
|  | |||||||
							
								
								
									
										28
									
								
								.eslintrc
									
									
									
									
									
								
							
							
						
						| @ -5,16 +5,32 @@ | |||||||
|     }, |     }, | ||||||
|     "plugins": [ |     "plugins": [ | ||||||
|       "css-modules", |       "css-modules", | ||||||
|  |       "jest", | ||||||
|  |       "jsx-a11y", | ||||||
|  |       "react", | ||||||
|  |       "react-hooks", | ||||||
|       "suggest-no-throw", |       "suggest-no-throw", | ||||||
|  |       "testing-library", | ||||||
|  |       "@typescript-eslint" | ||||||
|     ], |     ], | ||||||
|     "extends": [ |     "extends": [ | ||||||
|       "react-app", |       "plugin:css-modules/recommended", | ||||||
|       "react-app/jest", |       "plugin:jsx-a11y/recommended", | ||||||
|       "plugin:css-modules/recommended" |       "plugin:react-hooks/recommended" | ||||||
|     ], |     ], | ||||||
|     "rules": { |     "rules": { | ||||||
|       "@typescript-eslint/no-floating-promises": "error", |       "@typescript-eslint/no-floating-promises": "error", | ||||||
|       "@typescript-eslint/no-misused-promises": "error", |       "@typescript-eslint/no-misused-promises": "error", | ||||||
|  |       "jsx-a11y/click-events-have-key-events": "off", | ||||||
|  |       "jsx-a11y/no-autofocus": "off", | ||||||
|  |       "jsx-a11y/no-noninteractive-element-interactions": "off", | ||||||
|  |       "no-restricted-globals": [ | ||||||
|  |         "error", | ||||||
|  |         { | ||||||
|  |           "name": "isNaN", | ||||||
|  |           "message": "Use Number.isNaN() instead." | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|       "semi": [ |       "semi": [ | ||||||
|         "error", |         "error", | ||||||
|         "never" |         "never" | ||||||
| @ -25,6 +41,9 @@ | |||||||
|     "overrides": [ |     "overrides": [ | ||||||
|       { |       { | ||||||
|         "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure |         "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure | ||||||
|  |         "extends": [ | ||||||
|  |           "plugin:testing-library/react" | ||||||
|  |         ], | ||||||
|         "rules": { |         "rules": { | ||||||
|           "suggest-no-throw/suggest-no-throw": "off", |           "suggest-no-throw/suggest-no-throw": "off", | ||||||
|           "testing-library/prefer-screen-queries": "off", |           "testing-library/prefer-screen-queries": "off", | ||||||
| @ -33,6 +52,9 @@ | |||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         "files": ["src/**/*.test.ts"], |         "files": ["src/**/*.test.ts"], | ||||||
|  |         "extends": [ | ||||||
|  |           "plugin:testing-library/react" | ||||||
|  |         ], | ||||||
|         "rules": { |         "rules": { | ||||||
|           "suggest-no-throw/suggest-no-throw": "off", |           "suggest-no-throw/suggest-no-throw": "off", | ||||||
|         } |         } | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/ci-cd-scripts/playwright-electron.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -21,7 +21,7 @@ if [[ ! -f "test-results/.last-run.json" ]]; then | |||||||
| fi | fi | ||||||
|  |  | ||||||
| retry=1 | retry=1 | ||||||
| max_retrys=4 | max_retrys=5 | ||||||
|  |  | ||||||
| # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues | # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues | ||||||
| while [[ $retry -le $max_retrys ]]; do | while [[ $retry -le $max_retrys ]]; do | ||||||
|  | |||||||
							
								
								
									
										21
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -6,23 +6,36 @@ | |||||||
| version: 2 | version: 2 | ||||||
| updates: | updates: | ||||||
|   - package-ecosystem: 'npm' # See documentation for possible values |   - package-ecosystem: 'npm' # See documentation for possible values | ||||||
|       directory: '/' # Location of package manifests |     directories: | ||||||
|  |       - '/' | ||||||
|  |       - '/packages/codemirror-lang-kcl/' | ||||||
|  |       - '/packages/codemirror-lsp-client/' | ||||||
|     schedule: |     schedule: | ||||||
|           interval: 'weekly' |       interval: weekly | ||||||
|  |       day: monday | ||||||
|     reviewers: |     reviewers: | ||||||
|       - franknoirot |       - franknoirot | ||||||
|       - irev-dev |       - irev-dev | ||||||
|   - package-ecosystem: 'github-actions' # See documentation for possible values |   - package-ecosystem: 'github-actions' # See documentation for possible values | ||||||
|     directory: '/' # Location of package manifests |     directory: '/' # Location of package manifests | ||||||
|     schedule: |     schedule: | ||||||
|           interval: 'weekly' |       interval: weekly | ||||||
|  |       day: monday | ||||||
|     reviewers: |     reviewers: | ||||||
|       - adamchalmers |       - adamchalmers | ||||||
|       - jessfraz |       - jessfraz | ||||||
|   - package-ecosystem: 'cargo' # See documentation for possible values |   - package-ecosystem: 'cargo' # See documentation for possible values | ||||||
|     directory: '/src/wasm-lib/' # Location of package manifests |     directory: '/src/wasm-lib/' # Location of package manifests | ||||||
|     schedule: |     schedule: | ||||||
|           interval: 'weekly' |       interval: weekly | ||||||
|  |       day: monday | ||||||
|     reviewers: |     reviewers: | ||||||
|       - adamchalmers |       - adamchalmers | ||||||
|       - jessfraz |       - jessfraz | ||||||
|  |     groups: | ||||||
|  |       serde-dependencies: | ||||||
|  |         patterns: | ||||||
|  |           - "serde*" | ||||||
|  |       wasm-bindgen-deps: | ||||||
|  |         patterns: | ||||||
|  |           - "wasm-bindgen*" | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/build-and-store-wasm.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -27,7 +27,7 @@ jobs: | |||||||
|  |  | ||||||
|  |  | ||||||
|       # Upload the WASM bundle as an artifact |       # Upload the WASM bundle as an artifact | ||||||
|       - uses: actions/upload-artifact@v3 |       - uses: actions/upload-artifact@v4 | ||||||
|         with: |         with: | ||||||
|           name: wasm-bundle |           name: wasm-bundle | ||||||
|           path: src/wasm-lib/pkg |           path: src/wasm-lib/pkg | ||||||
|  | |||||||
							
								
								
									
										24
									
								
								.github/workflows/build-apps.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -126,7 +126,13 @@ jobs: | |||||||
|           node-version-file: '.nvmrc' |           node-version-file: '.nvmrc' | ||||||
|           cache: 'yarn' # Set this to npm, yarn or pnpm. |           cache: 'yarn' # Set this to npm, yarn or pnpm. | ||||||
|  |  | ||||||
|       - run: yarn install |       - name: yarn install | ||||||
|  |         # Windows is picky sometimes and fails on fetch. Step takes about ~30s | ||||||
|  |         uses: nick-fields/retry@v3.0.0 | ||||||
|  |         with: | ||||||
|  |           timeout_minutes: 2 | ||||||
|  |           max_attempts: 3 | ||||||
|  |           command: yarn install | ||||||
|  |  | ||||||
|       - run: yarn tronb:vite |       - run: yarn tronb:vite | ||||||
|  |  | ||||||
| @ -173,7 +179,13 @@ jobs: | |||||||
|           CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} |           CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | ||||||
|           CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} |           CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||||||
|           WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} |           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/ |       - name: List artifacts in out/ | ||||||
|         run: ls -R out |         run: ls -R out | ||||||
| @ -228,7 +240,13 @@ jobs: | |||||||
|           CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} |           CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | ||||||
|           CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} |           CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||||||
|           WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} |           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 |       - uses: actions/upload-artifact@v4 | ||||||
|         if: ${{ env.IS_RELEASE == 'true' }} |         if: ${{ env.IS_RELEASE == 'true' }} | ||||||
|  | |||||||
							
								
								
									
										44
									
								
								.github/workflows/cargo-bench.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -1,44 +0,0 @@ | |||||||
| on: |  | ||||||
|   push: |  | ||||||
|     branches: |  | ||||||
|       - main |  | ||||||
|     paths: |  | ||||||
|       - '**.rs' |  | ||||||
|       - '**/Cargo.toml' |  | ||||||
|       - '**/Cargo.lock' |  | ||||||
|       - '**/rust-toolchain.toml' |  | ||||||
|       - .github/workflows/cargo-bench.yml |  | ||||||
|   pull_request: |  | ||||||
|     paths: |  | ||||||
|       - '**.rs' |  | ||||||
|       - '**/Cargo.toml' |  | ||||||
|       - '**/Cargo.lock' |  | ||||||
|       - '**/rust-toolchain.toml' |  | ||||||
|       - .github/workflows/cargo-bench.yml |  | ||||||
|   workflow_dispatch: |  | ||||||
| permissions: read-all |  | ||||||
| concurrency: |  | ||||||
|   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} |  | ||||||
|   cancel-in-progress: true |  | ||||||
| name: cargo bench |  | ||||||
| jobs: |  | ||||||
|   cargo-bench: |  | ||||||
|     name: Benchmark with iai |  | ||||||
|     runs-on: ubuntu-latest-8-cores |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - uses: dtolnay/rust-toolchain@stable |  | ||||||
|       - name: Install dependencies |  | ||||||
|         run: | |  | ||||||
|           cargo install cargo-criterion |  | ||||||
|           sudo apt update |  | ||||||
|           sudo apt install -y valgrind |  | ||||||
|       - name: Rust Cache |  | ||||||
|         uses: Swatinem/rust-cache@v2.6.1 |  | ||||||
|       - name: Benchmark kcl library |  | ||||||
|         shell: bash |  | ||||||
|         run: |- |  | ||||||
|           cd src/wasm-lib/kcl; cargo bench --all-features -- iai |  | ||||||
|         env: |  | ||||||
|           KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}} |  | ||||||
|  |  | ||||||
							
								
								
									
										32
									
								
								.github/workflows/codemirror-lang-kcl.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,32 @@ | |||||||
|  | name: CodeMirror Lang KCL | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |  | ||||||
|  | concurrency: | ||||||
|  |   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | ||||||
|  |   cancel-in-progress: true | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   yarn-unit-test: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v4 | ||||||
|  |  | ||||||
|  |       - uses: actions/setup-node@v4 | ||||||
|  |         with: | ||||||
|  |           node-version-file: '.nvmrc' | ||||||
|  |           cache: 'yarn' | ||||||
|  |  | ||||||
|  |       - run: yarn install | ||||||
|  |         working-directory: packages/codemirror-lang-kcl | ||||||
|  |  | ||||||
|  |       - run: yarn tsc | ||||||
|  |         working-directory: packages/codemirror-lang-kcl | ||||||
|  |  | ||||||
|  |       - name: run unit tests | ||||||
|  |         run: yarn test | ||||||
|  |         working-directory: packages/codemirror-lang-kcl | ||||||
							
								
								
									
										8
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -18,7 +18,6 @@ permissions: | |||||||
| jobs: | jobs: | ||||||
|  |  | ||||||
|   check-rust-changes: |   check-rust-changes: | ||||||
|     if: github.event.pull_request.draft == false |  | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     outputs: |     outputs: | ||||||
|       rust-changed: ${{ steps.filter.outputs.rust }} |       rust-changed: ${{ steps.filter.outputs.rust }} | ||||||
| @ -35,7 +34,6 @@ jobs: | |||||||
|               - 'src/wasm-lib/**' |               - 'src/wasm-lib/**' | ||||||
|  |  | ||||||
|   electron: |   electron: | ||||||
|     if: github.event.pull_request.draft == false |  | ||||||
|     timeout-minutes: 60 |     timeout-minutes: 60 | ||||||
|     name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} |     name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} | ||||||
|     strategy: |     strategy: | ||||||
| @ -129,9 +127,12 @@ jobs: | |||||||
|       shell: bash |       shell: bash | ||||||
|       run: yarn tron:package |       run: yarn tron:package | ||||||
|     - name: Run ubuntu/chrome snapshots |     - name: Run ubuntu/chrome snapshots | ||||||
|  |       if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }} | ||||||
|       shell: bash |       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: | |       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: |       env: | ||||||
|         CI: true |         CI: true | ||||||
|         NODE_ENV: development |         NODE_ENV: development | ||||||
| @ -152,6 +153,7 @@ jobs: | |||||||
|       continue-on-error: true |       continue-on-error: true | ||||||
|       run: rm -r test-results |       run: rm -r test-results | ||||||
|     - name: check for changes |     - name: check for changes | ||||||
|  |       if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }} | ||||||
|       shell: bash |       shell: bash | ||||||
|       id: git-check |       id: git-check | ||||||
|       run: | |       run: | | ||||||
|  | |||||||
							
								
								
									
										40
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @ -337,13 +337,47 @@ For individual testing: | |||||||
| yarn test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false | 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 | ### 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 | 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). | Where `XXX` is an API token from the production engine (NOT the dev environment). | ||||||
|  | |||||||
							
								
								
									
										42
									
								
								docs/kcl/circleThreePoint.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										43
									
								
								docs/kcl/helixRevolutions.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -35,6 +35,7 @@ layout: manual | |||||||
| * [`ceil`](kcl/ceil) | * [`ceil`](kcl/ceil) | ||||||
| * [`chamfer`](kcl/chamfer) | * [`chamfer`](kcl/chamfer) | ||||||
| * [`circle`](kcl/circle) | * [`circle`](kcl/circle) | ||||||
|  | * [`circleThreePoint`](kcl/circleThreePoint) | ||||||
| * [`close`](kcl/close) | * [`close`](kcl/close) | ||||||
| * [`cm`](kcl/cm) | * [`cm`](kcl/cm) | ||||||
| * [`cos`](kcl/cos) | * [`cos`](kcl/cos) | ||||||
| @ -47,11 +48,11 @@ layout: manual | |||||||
| * [`getOppositeEdge`](kcl/getOppositeEdge) | * [`getOppositeEdge`](kcl/getOppositeEdge) | ||||||
| * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) | * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) | ||||||
| * [`helix`](kcl/helix) | * [`helix`](kcl/helix) | ||||||
|  | * [`helixRevolutions`](kcl/helixRevolutions) | ||||||
| * [`hole`](kcl/hole) | * [`hole`](kcl/hole) | ||||||
| * [`hollow`](kcl/hollow) | * [`hollow`](kcl/hollow) | ||||||
| * [`import`](kcl/import) | * [`import`](kcl/import) | ||||||
| * [`inch`](kcl/inch) | * [`inch`](kcl/inch) | ||||||
| * [`int`](kcl/int) |  | ||||||
| * [`lastSegX`](kcl/lastSegX) | * [`lastSegX`](kcl/lastSegX) | ||||||
| * [`lastSegY`](kcl/lastSegY) | * [`lastSegY`](kcl/lastSegY) | ||||||
| * [`legAngX`](kcl/legAngX) | * [`legAngX`](kcl/legAngX) | ||||||
| @ -80,6 +81,7 @@ layout: manual | |||||||
| * [`pi`](kcl/pi) | * [`pi`](kcl/pi) | ||||||
| * [`polar`](kcl/polar) | * [`polar`](kcl/polar) | ||||||
| * [`polygon`](kcl/polygon) | * [`polygon`](kcl/polygon) | ||||||
|  | * [`pop`](kcl/pop) | ||||||
| * [`pow`](kcl/pow) | * [`pow`](kcl/pow) | ||||||
| * [`profileStart`](kcl/profileStart) | * [`profileStart`](kcl/profileStart) | ||||||
| * [`profileStartX`](kcl/profileStartX) | * [`profileStartX`](kcl/profileStartX) | ||||||
|  | |||||||
| @ -4,6 +4,8 @@ excerpt: "Convert a number to an integer." | |||||||
| layout: manual | layout: manual | ||||||
| --- | --- | ||||||
|  |  | ||||||
|  | **WARNING:** This function is deprecated. | ||||||
|  |  | ||||||
| Convert a number to an integer. | Convert a number to an integer. | ||||||
|  |  | ||||||
| DEPRECATED use floor(), ceil(), or round(). | DEPRECATED use floor(), ceil(), or round(). | ||||||
|  | |||||||
							
								
								
									
										39
									
								
								docs/kcl/pop.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										29585
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
							
						
						| @ -1,19 +1,19 @@ | |||||||
| --- | --- | ||||||
| title: "AxisOrEdgeReference" | title: "Axis2dOrEdgeReference" | ||||||
| excerpt: "Axis or tagged edge." | excerpt: "A 2D axis or tagged edge." | ||||||
| layout: manual | layout: manual | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
| Axis or tagged edge. | A 2D axis or tagged edge. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| **This schema accepts any of the following:** | **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" | title: "AxisAndOrigin2d" | ||||||
| excerpt: "Axis and origin." | excerpt: "A 2D axis and origin." | ||||||
| layout: manual | 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 | | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								docs/kcl/types/Face.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,28 @@ | |||||||
|  | --- | ||||||
|  | title: "Face" | ||||||
|  | excerpt: "A face." | ||||||
|  | layout: manual | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | A face. | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `id` |`string`| The id of the face. | No | | ||||||
|  | | `value` |`string`| The tag of the face. | No | | ||||||
|  | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No | | ||||||
|  | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | | ||||||
|  | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||||
|  | | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A face. | No | | ||||||
|  | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										26
									
								
								docs/kcl/types/Helix.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,26 @@ | |||||||
|  | --- | ||||||
|  | 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 | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No | | ||||||
|  | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -1,10 +1,10 @@ | |||||||
| --- | --- | ||||||
| title: "HelixData" | title: "HelixData" | ||||||
| excerpt: "Data for helices." | excerpt: "Data for a helix." | ||||||
| layout: manual | layout: manual | ||||||
| --- | --- | ||||||
|  |  | ||||||
| Data for helices. | Data for a helix. | ||||||
|  |  | ||||||
| **Type:** `object` | **Type:** `object` | ||||||
|  |  | ||||||
| @ -19,6 +19,8 @@ Data for helices. | |||||||
| | `revolutions` |`number`| Number of revolutions. | No | | | `revolutions` |`number`| Number of revolutions. | No | | ||||||
| | `angleStart` |`number`| Start angle (in degrees). | No | | | `angleStart` |`number`| Start angle (in degrees). | No | | ||||||
| | `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | 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. This is not necessary if the helix is created around an edge. If not given the length of the edge is used. | 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 | | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										26
									
								
								docs/kcl/types/HelixValue.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,26 @@ | |||||||
|  | --- | ||||||
|  | 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 | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No | | ||||||
|  | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -168,7 +168,6 @@ Any KCL value. | |||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| A plane. |  | ||||||
|  |  | ||||||
| **Type:** `object` | **Type:** `object` | ||||||
|  |  | ||||||
| @ -181,17 +180,10 @@ A plane. | |||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `type` |enum: [`Plane`](/docs/kcl/types/Plane)|  | No | | | `type` |enum: [`Plane`](/docs/kcl/types/Plane)|  | No | | ||||||
| | `id` |`string`| The id of the plane. | No | | | `value` |[`Plane`](/docs/kcl/types/Plane)| Any KCL value. | No | | ||||||
| | `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No | |  | ||||||
| | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | |  | ||||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | |  | ||||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | |  | ||||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | |  | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| A face. |  | ||||||
|  |  | ||||||
| **Type:** `object` | **Type:** `object` | ||||||
|  |  | ||||||
| @ -203,14 +195,8 @@ A face. | |||||||
|  |  | ||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `type` |enum: `Face`|  | No | | | `type` |enum: [`Face`](/docs/kcl/types/Face)|  | No | | ||||||
| | `id` |`string`| The id of the face. | No | | | `value` |[`Face`](/docs/kcl/types/Face)| Any KCL value. | No | | ||||||
| | `value` |`string`| The tag of the face. | No | |  | ||||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No | |  | ||||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | |  | ||||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | |  | ||||||
| | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | |  | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| @ -246,7 +232,6 @@ A face. | |||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| An solid is a collection of extrude surfaces. |  | ||||||
|  |  | ||||||
| **Type:** `object` | **Type:** `object` | ||||||
|  |  | ||||||
| @ -259,14 +244,7 @@ An solid is a collection of extrude surfaces. | |||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `type` |enum: [`Solid`](/docs/kcl/types/Solid)|  | No | | | `type` |enum: [`Solid`](/docs/kcl/types/Solid)|  | No | | ||||||
| | `id` |`string`| The id of the solid. | No | | | `value` |[`Solid`](/docs/kcl/types/Solid)| Any KCL value. | No | | ||||||
| | `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No | |  | ||||||
| | `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No | |  | ||||||
| | `height` |`number`| The height of the solid. | No | |  | ||||||
| | `startCapId` |`string`| The id of the extrusion start cap | No | |  | ||||||
| | `endCapId` |`string`| The id of the extrusion end cap | No | |  | ||||||
| | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | |  | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| @ -285,6 +263,22 @@ An solid is a collection of extrude surfaces. | |||||||
| | `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`|  | No | | | `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: [`Helix`](/docs/kcl/types/Helix)|  | No | | ||||||
|  | | `value` |[`Helix`](/docs/kcl/types/Helix)| Any KCL value. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| Data for an imported geometry. | Data for an imported geometry. | ||||||
|  |  | ||||||
|  | |||||||
| @ -16,6 +16,6 @@ Data for a mirror. | |||||||
|  |  | ||||||
| | Property | Type | Description | Required | | | 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 | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ A plane. | |||||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | ||||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | ||||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A plane. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ Data for revolution surfaces. | |||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `angle` |`number` (**maximum:** 360.0) (**minimum:** -360.0)| Angle to revolve (in degrees). Default is 360. | No | | | `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 | | | `tolerance` |`number`| Tolerance for the revolve operation. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ A sketch is a collection of paths. | |||||||
| | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | ||||||
| | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | ||||||
| | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -30,6 +30,7 @@ A sketch is a collection of paths. | |||||||
| | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | ||||||
| | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | ||||||
| | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ A plane. | |||||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | ||||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | ||||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -54,6 +55,7 @@ A face. | |||||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | | ||||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||||
| | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ An solid is a collection of extrude surfaces. | |||||||
| | `startCapId` |`string`| The id of the extrusion start cap | No | | | `startCapId` |`string`| The id of the extrusion start cap | No | | ||||||
| | `endCapId` |`string`| The id of the extrusion end cap | No | | | `endCapId` |`string`| The id of the extrusion end cap | No | | ||||||
| | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| An solid is a collection of extrude surfaces. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -32,6 +32,7 @@ An solid is a collection of extrude surfaces. | |||||||
| | `startCapId` |`string`| The id of the extrusion start cap | No | | | `startCapId` |`string`| The id of the extrusion start cap | No | | ||||||
| | `endCapId` |`string`| The id of the extrusion end cap | No | | | `endCapId` |`string`| The id of the extrusion end cap | No | | ||||||
| | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A solid or a group of solids. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ Data for a sweep. | |||||||
|  |  | ||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No | | | `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 | | | `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 | | | `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) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										107
									
								
								docs/kcl/types/UnitLen.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,107 @@ | |||||||
|  | --- | ||||||
|  | title: "UnitLen" | ||||||
|  | excerpt: "" | ||||||
|  | layout: manual | ||||||
|  | --- | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **This schema accepts exactly one of the following:** | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: `Mm`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: `Cm`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: `M`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: `Inches`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: `Feet`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: `Yards`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -149,7 +149,7 @@ test.describe('Basic sketch', () => { | |||||||
|     await doBasicSketch(page, homePage, ['code']) |     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 |     // Load the app with the code panes | ||||||
|     await page.addInitScript(async (persistModelingContext) => { |     await page.addInitScript(async (persistModelingContext) => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|  | |||||||
| @ -280,7 +280,7 @@ test( | |||||||
|  |  | ||||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() |       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||||
|       await expect(page.getByText('router-template-slate')).toBeVisible() |       await expect(page.getByText('router-template-slate')).toBeVisible() | ||||||
|       await expect(page.getByText('New Project')).toBeVisible() |       await expect(page.getByText('Create project')).toBeVisible() | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await test.step('Opening the router-template project should load', async () => { |     await test.step('Opening the router-template project should load', async () => { | ||||||
|  | |||||||
| @ -45,46 +45,6 @@ test.describe('Command bar tests', () => { | |||||||
|     ) |     ) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   // TODO: fix this test after the electron migration |  | ||||||
|   test.fixme('Fillet from command bar', async ({ page, homePage }) => { |  | ||||||
|     await page.addInitScript(async () => { |  | ||||||
|       localStorage.setItem( |  | ||||||
|         'persistCode', |  | ||||||
|         `sketch001 = startSketchOn('XY') |  | ||||||
|     |> startProfileAt([-5, -5], %) |  | ||||||
|     |> line([0, 10], %) |  | ||||||
|     |> line([10, 0], %) |  | ||||||
|     |> line([0, -10], %) |  | ||||||
|     |> lineTo([profileStartX(%), profileStartY(%)], %) |  | ||||||
|     |> close(%) |  | ||||||
|   extrude001 = extrude(-10, sketch001)` |  | ||||||
|       ) |  | ||||||
|     }) |  | ||||||
|  |  | ||||||
|     const u = await getUtils(page) |  | ||||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) |  | ||||||
|     await homePage.goToModelingScene() |  | ||||||
|     await u.openDebugPanel() |  | ||||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') |  | ||||||
|     await u.closeDebugPanel() |  | ||||||
|  |  | ||||||
|     const selectSegment = () => page.getByText(`line([0, -10], %)`).click() |  | ||||||
|  |  | ||||||
|     await selectSegment() |  | ||||||
|     await page.waitForTimeout(100) |  | ||||||
|     await page.getByRole('button', { name: 'Fillet' }).click() |  | ||||||
|     await page.waitForTimeout(100) |  | ||||||
|     await page.keyboard.press('Enter') // skip selection |  | ||||||
|     await page.waitForTimeout(100) |  | ||||||
|     await page.keyboard.press('Enter') // accept default radius |  | ||||||
|     await page.waitForTimeout(100) |  | ||||||
|     await page.keyboard.press('Enter') // submit |  | ||||||
|     await page.waitForTimeout(100) |  | ||||||
|     await expect(page.locator('.cm-activeLine')).toContainText( |  | ||||||
|       `fillet({ radius = ${KCL_DEFAULT_LENGTH}, tags = [seg01] }, %)` |  | ||||||
|     ) |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   test('Command bar can change a setting, and switch back and forth between arguments', async ({ |   test('Command bar can change a setting, and switch back and forth between arguments', async ({ | ||||||
|     page, |     page, | ||||||
|     homePage, |     homePage, | ||||||
|  | |||||||
| @ -38,14 +38,14 @@ test.describe('Debug pane', () => { | |||||||
|       // Set the code in the code editor. |       // Set the code in the code editor. | ||||||
|       await u.codeLocator.click() |       await u.codeLocator.click() | ||||||
|       await page.keyboard.type(code, { delay: 0 }) |       await page.keyboard.type(code, { delay: 0 }) | ||||||
|       // Scroll to the feature tree. |       // Scroll to the artifact graph. | ||||||
|       await tree.scrollIntoViewIfNeeded() |       await tree.scrollIntoViewIfNeeded() | ||||||
|       // Expand the feature tree. |       // Expand the artifact graph. | ||||||
|       await tree.getByText('Feature Tree').click() |       await tree.getByText('Artifact Graph').click() | ||||||
|       // Just expanded the details, making the element taller, so scroll again. |       // Just expanded the details, making the element taller, so scroll again. | ||||||
|       await tree.getByText('Plane').first().scrollIntoViewIfNeeded() |       await tree.getByText('Plane').first().scrollIntoViewIfNeeded() | ||||||
|     }) |     }) | ||||||
|     // Extract the artifact IDs from the debug feature tree. |     // Extract the artifact IDs from the debug artifact graph. | ||||||
|     const initialSegmentIds = await segment.innerText({ timeout: 5_000 }) |     const initialSegmentIds = await segment.innerText({ timeout: 5_000 }) | ||||||
|     // The artifact ID should include a UUID. |     // The artifact ID should include a UUID. | ||||||
|     expect(initialSegmentIds).toMatch( |     expect(initialSegmentIds).toMatch( | ||||||
|  | |||||||
| @ -76,7 +76,7 @@ test.describe('Editor tests', () => { | |||||||
|     await u.openDebugPanel() |     await u.openDebugPanel() | ||||||
|     await expect( |     await expect( | ||||||
|       page.locator('[data-receive-command-type="scene_clear_all"]') |       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||||
|     ).toHaveCount(2) |     ).toHaveCount(1) | ||||||
|     await expect( |     await expect( | ||||||
|       page.locator('[data-message-type="execution-done"]') |       page.locator('[data-message-type="execution-done"]') | ||||||
|     ).toHaveCount(2) |     ).toHaveCount(2) | ||||||
| @ -100,7 +100,60 @@ test.describe('Editor tests', () => { | |||||||
|     ).toHaveCount(3) |     ).toHaveCount(3) | ||||||
|     await expect( |     await expect( | ||||||
|       page.locator('[data-receive-command-type="scene_clear_all"]') |       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) |     ).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 ({ |   test('if you click the format button it formats your code', async ({ | ||||||
|  | |||||||
							
								
								
									
										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() | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   ) | ||||||
|  | }) | ||||||
| @ -1,4 +1,4 @@ | |||||||
| import type { Page } from '@playwright/test' | import type { Page, Locator } from '@playwright/test' | ||||||
| import { expect } from '@playwright/test' | import { expect } from '@playwright/test' | ||||||
|  |  | ||||||
| type CmdBarSerialised = | type CmdBarSerialised = | ||||||
| @ -26,9 +26,11 @@ type CmdBarSerialised = | |||||||
|  |  | ||||||
| export class CmdBarFixture { | export class CmdBarFixture { | ||||||
|   public page: Page |   public page: Page | ||||||
|  |   cmdBarOpenBtn!: Locator | ||||||
|  |  | ||||||
|   constructor(page: Page) { |   constructor(page: Page) { | ||||||
|     this.page = page |     this.page = page | ||||||
|  |     this.cmdBarOpenBtn = page.getByTestId('command-bar-open-button') | ||||||
|   } |   } | ||||||
|   reConstruct = (page: Page) => { |   reConstruct = (page: Page) => { | ||||||
|     this.page = page |     this.page = page | ||||||
| @ -116,4 +118,37 @@ export class CmdBarFixture { | |||||||
|       await this.page.keyboard.press('Enter') |       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() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get cmdSearchInput() { | ||||||
|  |     return this.page.getByTestId('cmd-bar-search') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get argumentInput() { | ||||||
|  |     return this.page.getByTestId('cmd-bar-arg-value') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get cmdOptions() { | ||||||
|  |     return this.page.getByTestId('cmd-bar-option') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   chooseCommand = async (commandName: string) => { | ||||||
|  |     await this.cmdOptions.getByText(commandName).click() | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ export class EditorFixture { | |||||||
|   private diagnosticsTooltip!: Locator |   private diagnosticsTooltip!: Locator | ||||||
|   private diagnosticsGutterIcon!: Locator |   private diagnosticsGutterIcon!: Locator | ||||||
|   private codeContent!: Locator |   private codeContent!: Locator | ||||||
|   private activeLine!: Locator |   public activeLine!: Locator | ||||||
|  |  | ||||||
|   constructor(page: Page) { |   constructor(page: Page) { | ||||||
|     this.page = page |     this.page = page | ||||||
|  | |||||||
| @ -121,18 +121,23 @@ export class AuthenticatedTronApp { | |||||||
|  |  | ||||||
| export const fixtures = { | export const fixtures = { | ||||||
|   cmdBar: async ({ page }: { page: Page }, use: any) => { |   cmdBar: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new CmdBarFixture(page)) |     await use(new CmdBarFixture(page)) | ||||||
|   }, |   }, | ||||||
|   editor: async ({ page }: { page: Page }, use: any) => { |   editor: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new EditorFixture(page)) |     await use(new EditorFixture(page)) | ||||||
|   }, |   }, | ||||||
|   toolbar: async ({ page }: { page: Page }, use: any) => { |   toolbar: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new ToolbarFixture(page)) |     await use(new ToolbarFixture(page)) | ||||||
|   }, |   }, | ||||||
|   scene: async ({ page }: { page: Page }, use: any) => { |   scene: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new SceneFixture(page)) |     await use(new SceneFixture(page)) | ||||||
|   }, |   }, | ||||||
|   homePage: async ({ page }: { page: Page }, use: any) => { |   homePage: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new HomePageFixture(page)) |     await use(new HomePageFixture(page)) | ||||||
|   }, |   }, | ||||||
| } | } | ||||||
|  | |||||||
| @ -103,7 +103,7 @@ export class HomePageFixture { | |||||||
|       .toEqual(expectedState) |       .toEqual(expectedState) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   createAndGoToProject = async (projectTitle: string) => { |   createAndGoToProject = async (projectTitle = 'project-$nnn') => { | ||||||
|     await expect(this.projectSection).not.toHaveText('Loading your Projects...') |     await expect(this.projectSection).not.toHaveText('Loading your Projects...') | ||||||
|     await this.projectButtonNew.click() |     await this.projectButtonNew.click() | ||||||
|     await this.projectTextName.click() |     await this.projectTextName.click() | ||||||
|  | |||||||
| @ -36,7 +36,8 @@ type DragFromHandler = ( | |||||||
|  |  | ||||||
| export class SceneFixture { | export class SceneFixture { | ||||||
|   public page: Page |   public page: Page | ||||||
|  |   public streamWrapper!: Locator | ||||||
|  |   public loadingIndicator!: Locator | ||||||
|   private exeIndicator!: Locator |   private exeIndicator!: Locator | ||||||
|  |  | ||||||
|   constructor(page: Page) { |   constructor(page: Page) { | ||||||
| @ -64,6 +65,8 @@ export class SceneFixture { | |||||||
|     this.page = page |     this.page = page | ||||||
|  |  | ||||||
|     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') |     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') | ||||||
|  |     this.streamWrapper = page.getByTestId('stream') | ||||||
|  |     this.loadingIndicator = this.streamWrapper.getByTestId('loading') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   makeMouseHelpers = ( |   makeMouseHelpers = ( | ||||||
|  | |||||||
| @ -1,12 +1,22 @@ | |||||||
| import type { Page, Locator } from '@playwright/test' | import type { Page, Locator } from '@playwright/test' | ||||||
| import { expect } from '../zoo-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 { | export class ToolbarFixture { | ||||||
|   public page: Page |   public page: Page | ||||||
|  |  | ||||||
|   extrudeButton!: Locator |   extrudeButton!: Locator | ||||||
|   loftButton!: Locator |   loftButton!: Locator | ||||||
|  |   sweepButton!: Locator | ||||||
|  |   filletButton!: Locator | ||||||
|  |   chamferButton!: Locator | ||||||
|   shellButton!: Locator |   shellButton!: Locator | ||||||
|   offsetPlaneButton!: Locator |   offsetPlaneButton!: Locator | ||||||
|   startSketchBtn!: Locator |   startSketchBtn!: Locator | ||||||
| @ -20,6 +30,10 @@ export class ToolbarFixture { | |||||||
|   filePane!: Locator |   filePane!: Locator | ||||||
|   exeIndicator!: Locator |   exeIndicator!: Locator | ||||||
|   treeInputField!: 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) { |   constructor(page: Page) { | ||||||
|     this.page = page |     this.page = page | ||||||
| @ -29,6 +43,9 @@ export class ToolbarFixture { | |||||||
|     this.page = page |     this.page = page | ||||||
|     this.extrudeButton = page.getByTestId('extrude') |     this.extrudeButton = page.getByTestId('extrude') | ||||||
|     this.loftButton = page.getByTestId('loft') |     this.loftButton = page.getByTestId('loft') | ||||||
|  |     this.sweepButton = page.getByTestId('sweep') | ||||||
|  |     this.filletButton = page.getByTestId('fillet3d') | ||||||
|  |     this.chamferButton = page.getByTestId('chamfer3d') | ||||||
|     this.shellButton = page.getByTestId('shell') |     this.shellButton = page.getByTestId('shell') | ||||||
|     this.offsetPlaneButton = page.getByTestId('plane-offset') |     this.offsetPlaneButton = page.getByTestId('plane-offset') | ||||||
|     this.startSketchBtn = page.getByTestId('sketch') |     this.startSketchBtn = page.getByTestId('sketch') | ||||||
| @ -41,10 +58,15 @@ export class ToolbarFixture { | |||||||
|     this.treeInputField = page.getByTestId('tree-input-field') |     this.treeInputField = page.getByTestId('tree-input-field') | ||||||
|  |  | ||||||
|     this.filePane = page.locator('#files-pane') |     this.filePane = page.locator('#files-pane') | ||||||
|  |     this.featureTreePane = page.locator('#feature-tree-pane') | ||||||
|     this.fileCreateToast = page.getByText('Successfully created') |     this.fileCreateToast = page.getByText('Successfully created') | ||||||
|     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') |     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get logoLink() { | ||||||
|  |     return this.page.getByTestId('app-logo') | ||||||
|  |   } | ||||||
|  |  | ||||||
|   startSketchPlaneSelection = async () => |   startSketchPlaneSelection = async () => | ||||||
|     doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500) |     doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500) | ||||||
|  |  | ||||||
| @ -91,4 +113,76 @@ export class ToolbarFixture { | |||||||
|       await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 }) |       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() | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -756,6 +756,17 @@ test(`Offset plane point-and-click`, async ({ | |||||||
|     }) |     }) | ||||||
|     await scene.expectPixelColor([74, 74, 74], testPoint, 15) |     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 = [ | const loftPointAndClickCases = [ | ||||||
| @ -818,12 +829,6 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => { | |||||||
|         }) |         }) | ||||||
|         await selectSketches() |         await selectSketches() | ||||||
|         await cmdBar.progressCmdBar() |         await cmdBar.progressCmdBar() | ||||||
|         await cmdBar.expectState({ |  | ||||||
|           stage: 'review', |  | ||||||
|           headerArguments: { Selection: '2 faces' }, |  | ||||||
|           commandName: 'Loft', |  | ||||||
|         }) |  | ||||||
|         await cmdBar.progressCmdBar() |  | ||||||
|       }) |       }) | ||||||
|     } else { |     } else { | ||||||
|       await test.step(`Preselect the two sketches`, async () => { |       await test.step(`Preselect the two sketches`, async () => { | ||||||
| @ -833,12 +838,6 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => { | |||||||
|       await test.step(`Go through the command bar flow with preselected sketches`, async () => { |       await test.step(`Go through the command bar flow with preselected sketches`, async () => { | ||||||
|         await toolbar.loftButton.click() |         await toolbar.loftButton.click() | ||||||
|         await cmdBar.progressCmdBar() |         await cmdBar.progressCmdBar() | ||||||
|         await cmdBar.expectState({ |  | ||||||
|           stage: 'review', |  | ||||||
|           headerArguments: { Selection: '2 faces' }, |  | ||||||
|           commandName: 'Loft', |  | ||||||
|         }) |  | ||||||
|         await cmdBar.progressCmdBar() |  | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -851,6 +850,666 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => { | |||||||
|       }) |       }) | ||||||
|       await scene.expectPixelColor([89, 89, 89], testPoint, 15) |       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: 'target', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Target: '', | ||||||
|  |         Trajectory: '', | ||||||
|  |       }, | ||||||
|  |       highlightedHeaderArg: 'target', | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await clickOnSketch1() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Sweep', | ||||||
|  |       currentArgKey: 'trajectory', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Target: '1 face', | ||||||
|  |         Trajectory: '', | ||||||
|  |       }, | ||||||
|  |       highlightedHeaderArg: 'trajectory', | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await clickOnSketch2() | ||||||
|  |     await page.waitForTimeout(500) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |     await page.waitForTimeout(500) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   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) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | test(`Sweep point-and-click failing validation`, async ({ | ||||||
|  |   context, | ||||||
|  |   page, | ||||||
|  |   homePage, | ||||||
|  |   scene, | ||||||
|  |   toolbar, | ||||||
|  |   cmdBar, | ||||||
|  | }) => { | ||||||
|  |   const initialCode = `sketch001 = startSketchOn('YZ') | ||||||
|  |   |> circle({ | ||||||
|  |        center = [0, 0], | ||||||
|  |        radius = 500 | ||||||
|  |      }, %) | ||||||
|  | sketch002 = startSketchOn('XZ') | ||||||
|  |   |> startProfileAt([0, 0], %) | ||||||
|  |   |> xLine(-500, %) | ||||||
|  |   |> lineTo([-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) | ||||||
|  |  | ||||||
|  |   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 and fail validation with a toast`, async () => { | ||||||
|  |     await toolbar.sweepButton.click() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Sweep', | ||||||
|  |       currentArgKey: 'target', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Target: '', | ||||||
|  |         Trajectory: '', | ||||||
|  |       }, | ||||||
|  |       highlightedHeaderArg: 'target', | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await clickOnSketch1() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Sweep', | ||||||
|  |       currentArgKey: 'trajectory', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Target: '1 face', | ||||||
|  |         Trajectory: '', | ||||||
|  |       }, | ||||||
|  |       highlightedHeaderArg: 'trajectory', | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await clickOnSketch2() | ||||||
|  |     await page.waitForTimeout(500) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |     await expect( | ||||||
|  |       page.getByText('Unable to sweep with the provided selection') | ||||||
|  |     ).toBeVisible() | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | test(`Fillet point-and-click`, async ({ | ||||||
|  |   context, | ||||||
|  |   page, | ||||||
|  |   homePage, | ||||||
|  |   scene, | ||||||
|  |   editor, | ||||||
|  |   toolbar, | ||||||
|  |   cmdBar, | ||||||
|  | }) => { | ||||||
|  |   // Code samples | ||||||
|  |   const initialCode = `sketch001 = startSketchOn('XY') | ||||||
|  |   |> startProfileAt([-12, -6], %) | ||||||
|  |   |> line([0, 12], %) | ||||||
|  |   |> line([24, 0], %) | ||||||
|  |   |> line([0, -12], %) | ||||||
|  |   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||||
|  |   |> close(%) | ||||||
|  | extrude001 = extrude(-12, sketch001) | ||||||
|  | ` | ||||||
|  |   const firstFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)' | ||||||
|  |   const secondFilletDeclaration = | ||||||
|  |     'fillet({       radius = 5,       tags = [getOppositeEdge(seg01)]     }, %)' | ||||||
|  |  | ||||||
|  |   // Locators | ||||||
|  |   const firstEdgeLocation = { x: 600, y: 193 } | ||||||
|  |   const secondEdgeLocation = { x: 600, y: 383 } | ||||||
|  |   const bodyLocation = { x: 630, y: 290 } | ||||||
|  |   const [clickOnFirstEdge] = scene.makeMouseHelpers( | ||||||
|  |     firstEdgeLocation.x, | ||||||
|  |     firstEdgeLocation.y | ||||||
|  |   ) | ||||||
|  |   const [clickOnSecondEdge] = scene.makeMouseHelpers( | ||||||
|  |     secondEdgeLocation.x, | ||||||
|  |     secondEdgeLocation.y | ||||||
|  |   ) | ||||||
|  |  | ||||||
|  |   // Colors | ||||||
|  |   const edgeColorWhite: [number, number, number] = [248, 248, 248] | ||||||
|  |   const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12 | ||||||
|  |   const bodyColor: [number, number, number] = [155, 155, 155] | ||||||
|  |   const filletColor: [number, number, number] = [127, 127, 127] | ||||||
|  |   const backgroundColor: [number, number, number] = [30, 30, 30] | ||||||
|  |   const lowTolerance = 20 | ||||||
|  |   const highTolerance = 40 | ||||||
|  |  | ||||||
|  |   // Setup | ||||||
|  |   await test.step(`Initial test setup`, async () => { | ||||||
|  |     await context.addInitScript((initialCode) => { | ||||||
|  |       localStorage.setItem('persistCode', initialCode) | ||||||
|  |     }, initialCode) | ||||||
|  |     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||||
|  |     await homePage.goToModelingScene() | ||||||
|  |  | ||||||
|  |     // verify modeling scene is loaded | ||||||
|  |     await scene.expectPixelColor( | ||||||
|  |       backgroundColor, | ||||||
|  |       secondEdgeLocation, | ||||||
|  |       lowTolerance | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // wait for stream to load | ||||||
|  |     await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   // Test 1: Command bar flow with preselected edges | ||||||
|  |   await test.step(`Select first edge`, async () => { | ||||||
|  |     await scene.expectPixelColor( | ||||||
|  |       edgeColorWhite, | ||||||
|  |       firstEdgeLocation, | ||||||
|  |       lowTolerance | ||||||
|  |     ) | ||||||
|  |     await clickOnFirstEdge() | ||||||
|  |     await scene.expectPixelColor( | ||||||
|  |       edgeColorYellow, | ||||||
|  |       firstEdgeLocation, | ||||||
|  |       highTolerance // Ubuntu color mismatch can require high tolerance | ||||||
|  |     ) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Apply fillet to the preselected edge`, async () => { | ||||||
|  |     await page.waitForTimeout(100) | ||||||
|  |     await toolbar.filletButton.click() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Fillet', | ||||||
|  |       highlightedHeaderArg: 'selection', | ||||||
|  |       currentArgKey: 'selection', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '', | ||||||
|  |         Radius: '', | ||||||
|  |       }, | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Fillet', | ||||||
|  |       highlightedHeaderArg: 'radius', | ||||||
|  |       currentArgKey: 'radius', | ||||||
|  |       currentArgValue: '5', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '1 face', | ||||||
|  |         Radius: '', | ||||||
|  |       }, | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Fillet', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '1 face', | ||||||
|  |         Radius: '5', | ||||||
|  |       }, | ||||||
|  |       stage: 'review', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Confirm code is added to the editor`, async () => { | ||||||
|  |     await editor.expectEditor.toContain(firstFilletDeclaration) | ||||||
|  |     await editor.expectState({ | ||||||
|  |       diagnostics: [], | ||||||
|  |       activeLines: ['|>fillet({radius=5,tags=[seg01]},%)'], | ||||||
|  |       highlightedCode: '', | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Confirm scene has changed`, async () => { | ||||||
|  |     await scene.expectPixelColor(filletColor, firstEdgeLocation, lowTolerance) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   // Test 2: Command bar flow without preselected edges | ||||||
|  |   await test.step(`Open fillet UI without selecting edges`, async () => { | ||||||
|  |     await page.waitForTimeout(100) | ||||||
|  |     await toolbar.filletButton.click() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       stage: 'arguments', | ||||||
|  |       currentArgKey: 'selection', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '', | ||||||
|  |         Radius: '', | ||||||
|  |       }, | ||||||
|  |       highlightedHeaderArg: 'selection', | ||||||
|  |       commandName: 'Fillet', | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Select second edge`, async () => { | ||||||
|  |     await scene.expectPixelColor( | ||||||
|  |       edgeColorWhite, | ||||||
|  |       secondEdgeLocation, | ||||||
|  |       lowTolerance | ||||||
|  |     ) | ||||||
|  |     await clickOnSecondEdge() | ||||||
|  |     await scene.expectPixelColor( | ||||||
|  |       edgeColorYellow, | ||||||
|  |       secondEdgeLocation, | ||||||
|  |       highTolerance // Ubuntu color mismatch can require high tolerance | ||||||
|  |     ) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Apply fillet to the second edge`, async () => { | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Fillet', | ||||||
|  |       highlightedHeaderArg: 'selection', | ||||||
|  |       currentArgKey: 'selection', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '', | ||||||
|  |         Radius: '', | ||||||
|  |       }, | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Fillet', | ||||||
|  |       highlightedHeaderArg: 'radius', | ||||||
|  |       currentArgKey: 'radius', | ||||||
|  |       currentArgValue: '5', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '1 sweepEdge', | ||||||
|  |         Radius: '', | ||||||
|  |       }, | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Fillet', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '1 sweepEdge', | ||||||
|  |         Radius: '5', | ||||||
|  |       }, | ||||||
|  |       stage: 'review', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Confirm code is added to the editor`, async () => { | ||||||
|  |     await editor.expectEditor.toContain(secondFilletDeclaration) | ||||||
|  |     await editor.expectState({ | ||||||
|  |       diagnostics: [], | ||||||
|  |       activeLines: ['radius=5,'], | ||||||
|  |       highlightedCode: '', | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Confirm scene has changed`, async () => { | ||||||
|  |     await scene.expectPixelColor( | ||||||
|  |       backgroundColor, | ||||||
|  |       secondEdgeLocation, | ||||||
|  |       lowTolerance | ||||||
|  |     ) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | test(`Chamfer point-and-click`, async ({ | ||||||
|  |   context, | ||||||
|  |   page, | ||||||
|  |   homePage, | ||||||
|  |   scene, | ||||||
|  |   editor, | ||||||
|  |   toolbar, | ||||||
|  |   cmdBar, | ||||||
|  | }) => { | ||||||
|  |   // Code samples | ||||||
|  |   const initialCode = `sketch001 = startSketchOn('XY') | ||||||
|  |   |> startProfileAt([-12, -6], %) | ||||||
|  |   |> line([0, 12], %) | ||||||
|  |   |> line([24, 0], %) | ||||||
|  |   |> line([0, -12], %) | ||||||
|  |   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||||
|  |   |> close(%) | ||||||
|  | extrude001 = extrude(-12, sketch001) | ||||||
|  | ` | ||||||
|  |   const firstChamferDeclaration = 'chamfer({ length = 5, tags = [seg01] }, %)' | ||||||
|  |   const secondChamferDeclaration = | ||||||
|  |     'chamfer({       length = 5,       tags = [getOppositeEdge(seg01)]     }, %)' | ||||||
|  |  | ||||||
|  |   // Locators | ||||||
|  |   const firstEdgeLocation = { x: 600, y: 193 } | ||||||
|  |   const secondEdgeLocation = { x: 600, y: 383 } | ||||||
|  |   const bodyLocation = { x: 630, y: 290 } | ||||||
|  |   const [clickOnFirstEdge] = scene.makeMouseHelpers( | ||||||
|  |     firstEdgeLocation.x, | ||||||
|  |     firstEdgeLocation.y | ||||||
|  |   ) | ||||||
|  |   const [clickOnSecondEdge] = scene.makeMouseHelpers( | ||||||
|  |     secondEdgeLocation.x, | ||||||
|  |     secondEdgeLocation.y | ||||||
|  |   ) | ||||||
|  |  | ||||||
|  |   // Colors | ||||||
|  |   const edgeColorWhite: [number, number, number] = [248, 248, 248] | ||||||
|  |   const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12 | ||||||
|  |   const bodyColor: [number, number, number] = [155, 155, 155] | ||||||
|  |   const chamferColor: [number, number, number] = [168, 168, 168] | ||||||
|  |   const backgroundColor: [number, number, number] = [30, 30, 30] | ||||||
|  |   const lowTolerance = 20 | ||||||
|  |   const highTolerance = 40 | ||||||
|  |  | ||||||
|  |   // Setup | ||||||
|  |   await test.step(`Initial test setup`, async () => { | ||||||
|  |     await context.addInitScript((initialCode) => { | ||||||
|  |       localStorage.setItem('persistCode', initialCode) | ||||||
|  |     }, initialCode) | ||||||
|  |     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||||
|  |     await homePage.goToModelingScene() | ||||||
|  |  | ||||||
|  |     // verify modeling scene is loaded | ||||||
|  |     await scene.expectPixelColor( | ||||||
|  |       backgroundColor, | ||||||
|  |       secondEdgeLocation, | ||||||
|  |       lowTolerance | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // wait for stream to load | ||||||
|  |     await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   // Test 1: Command bar flow with preselected edges | ||||||
|  |   await test.step(`Select first edge`, async () => { | ||||||
|  |     await scene.expectPixelColor( | ||||||
|  |       edgeColorWhite, | ||||||
|  |       firstEdgeLocation, | ||||||
|  |       lowTolerance | ||||||
|  |     ) | ||||||
|  |     await clickOnFirstEdge() | ||||||
|  |     await scene.expectPixelColor( | ||||||
|  |       edgeColorYellow, | ||||||
|  |       firstEdgeLocation, | ||||||
|  |       highTolerance // Ubuntu color mismatch can require high tolerance | ||||||
|  |     ) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Apply chamfer to the preselected edge`, async () => { | ||||||
|  |     await page.waitForTimeout(100) | ||||||
|  |     await toolbar.chamferButton.click() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Chamfer', | ||||||
|  |       highlightedHeaderArg: 'selection', | ||||||
|  |       currentArgKey: 'selection', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '', | ||||||
|  |         Length: '', | ||||||
|  |       }, | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Chamfer', | ||||||
|  |       highlightedHeaderArg: 'length', | ||||||
|  |       currentArgKey: 'length', | ||||||
|  |       currentArgValue: '5', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '1 face', | ||||||
|  |         Length: '', | ||||||
|  |       }, | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Chamfer', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '1 face', | ||||||
|  |         Length: '5', | ||||||
|  |       }, | ||||||
|  |       stage: 'review', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Confirm code is added to the editor`, async () => { | ||||||
|  |     await editor.expectEditor.toContain(firstChamferDeclaration) | ||||||
|  |     await editor.expectState({ | ||||||
|  |       diagnostics: [], | ||||||
|  |       activeLines: ['|>chamfer({length=5,tags=[seg01]},%)'], | ||||||
|  |       highlightedCode: '', | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Confirm scene has changed`, async () => { | ||||||
|  |     await scene.expectPixelColor(chamferColor, firstEdgeLocation, lowTolerance) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   // Test 2: Command bar flow without preselected edges | ||||||
|  |   await test.step(`Open chamfer UI without selecting edges`, async () => { | ||||||
|  |     await page.waitForTimeout(100) | ||||||
|  |     await toolbar.chamferButton.click() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       stage: 'arguments', | ||||||
|  |       currentArgKey: 'selection', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '', | ||||||
|  |         Length: '', | ||||||
|  |       }, | ||||||
|  |       highlightedHeaderArg: 'selection', | ||||||
|  |       commandName: 'Chamfer', | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Select second edge`, async () => { | ||||||
|  |     await scene.expectPixelColor( | ||||||
|  |       edgeColorWhite, | ||||||
|  |       secondEdgeLocation, | ||||||
|  |       lowTolerance | ||||||
|  |     ) | ||||||
|  |     await clickOnSecondEdge() | ||||||
|  |     await scene.expectPixelColor( | ||||||
|  |       edgeColorYellow, | ||||||
|  |       secondEdgeLocation, | ||||||
|  |       highTolerance // Ubuntu color mismatch can require high tolerance | ||||||
|  |     ) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Apply chamfer to the second edge`, async () => { | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Chamfer', | ||||||
|  |       highlightedHeaderArg: 'selection', | ||||||
|  |       currentArgKey: 'selection', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '', | ||||||
|  |         Length: '', | ||||||
|  |       }, | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Chamfer', | ||||||
|  |       highlightedHeaderArg: 'length', | ||||||
|  |       currentArgKey: 'length', | ||||||
|  |       currentArgValue: '5', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '1 sweepEdge', | ||||||
|  |         Length: '', | ||||||
|  |       }, | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Chamfer', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '1 sweepEdge', | ||||||
|  |         Length: '5', | ||||||
|  |       }, | ||||||
|  |       stage: 'review', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Confirm code is added to the editor`, async () => { | ||||||
|  |     await editor.expectEditor.toContain(secondChamferDeclaration) | ||||||
|  |     await editor.expectState({ | ||||||
|  |       diagnostics: [], | ||||||
|  |       activeLines: ['length=5,'], | ||||||
|  |       highlightedCode: '', | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Confirm scene has changed`, async () => { | ||||||
|  |     await scene.expectPixelColor( | ||||||
|  |       backgroundColor, | ||||||
|  |       secondEdgeLocation, | ||||||
|  |       lowTolerance | ||||||
|  |     ) | ||||||
|   }) |   }) | ||||||
| }) | }) | ||||||
|  |  | ||||||
| @ -907,6 +1566,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { | |||||||
|         await clickOnCap() |         await clickOnCap() | ||||||
|         await page.waitForTimeout(500) |         await page.waitForTimeout(500) | ||||||
|         await cmdBar.progressCmdBar() |         await cmdBar.progressCmdBar() | ||||||
|  |         await page.waitForTimeout(500) | ||||||
|         await cmdBar.progressCmdBar() |         await cmdBar.progressCmdBar() | ||||||
|         await cmdBar.expectState({ |         await cmdBar.expectState({ | ||||||
|           stage: 'review', |           stage: 'review', | ||||||
| @ -927,6 +1587,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { | |||||||
|       await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => { |       await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => { | ||||||
|         await toolbar.shellButton.click() |         await toolbar.shellButton.click() | ||||||
|         await cmdBar.progressCmdBar() |         await cmdBar.progressCmdBar() | ||||||
|  |         await page.waitForTimeout(500) | ||||||
|         await cmdBar.progressCmdBar() |         await cmdBar.progressCmdBar() | ||||||
|         await cmdBar.expectState({ |         await cmdBar.expectState({ | ||||||
|           stage: 'review', |           stage: 'review', | ||||||
| @ -1008,6 +1669,7 @@ extrude001 = extrude(40, sketch001) | |||||||
|     await page.waitForTimeout(500) |     await page.waitForTimeout(500) | ||||||
|     await page.keyboard.up('Shift') |     await page.keyboard.up('Shift') | ||||||
|     await cmdBar.progressCmdBar() |     await cmdBar.progressCmdBar() | ||||||
|  |     await page.waitForTimeout(500) | ||||||
|     await cmdBar.progressCmdBar() |     await cmdBar.progressCmdBar() | ||||||
|     await cmdBar.expectState({ |     await cmdBar.expectState({ | ||||||
|       stage: 'review', |       stage: 'review', | ||||||
| @ -1030,4 +1692,162 @@ extrude001 = extrude(40, sketch001) | |||||||
|     }) |     }) | ||||||
|     await scene.expectPixelColor([49, 49, 49], testPoint, 15) |     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) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | test(`Shell dry-run validation rejects sweeps`, 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(-2000, %) | ||||||
|  | sweep001 = sweep({ path = sketch002 }, sketch001) | ||||||
|  | ` | ||||||
|  |   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: 500, y: 250 } | ||||||
|  |   const [clickOnSweep] = scene.makeMouseHelpers(testPoint.x, testPoint.y) | ||||||
|  |  | ||||||
|  |   await test.step(`Confirm sweep exists`, async () => { | ||||||
|  |     await toolbar.closePane('code') | ||||||
|  |     await scene.expectPixelColor([231, 231, 231], testPoint, 15) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   await test.step(`Go through the Shell flow and fail validation with a toast`, async () => { | ||||||
|  |     await toolbar.shellButton.click() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       stage: 'arguments', | ||||||
|  |       currentArgKey: 'selection', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Selection: '', | ||||||
|  |         Thickness: '', | ||||||
|  |       }, | ||||||
|  |       highlightedHeaderArg: 'selection', | ||||||
|  |       commandName: 'Shell', | ||||||
|  |     }) | ||||||
|  |     await clickOnSweep() | ||||||
|  |     await page.waitForTimeout(500) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |     await expect( | ||||||
|  |       page.getByText('Unable to shell with the provided selection') | ||||||
|  |     ).toBeVisible() | ||||||
|  |     await page.waitForTimeout(1000) | ||||||
|  |   }) | ||||||
| }) | }) | ||||||
|  | |||||||
| @ -115,7 +115,7 @@ test( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| 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' }, |   { tag: '@electron' }, | ||||||
|   async ({ context, page }, testInfo) => { |   async ({ context, page }, testInfo) => { | ||||||
|     await context.folderSetupFn(async (dir) => { |     await context.folderSetupFn(async (dir) => { | ||||||
| @ -172,7 +172,7 @@ test( | |||||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() |       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||||
|       await expect(page.getByText('broken-code')).toBeVisible() |       await expect(page.getByText('broken-code')).toBeVisible() | ||||||
|       await expect(page.getByText('bracket')).toBeVisible() |       await expect(page.getByText('bracket')).toBeVisible() | ||||||
|       await expect(page.getByText('New Project')).toBeVisible() |       await expect(page.getByText('Create project')).toBeVisible() | ||||||
|     }) |     }) | ||||||
|     await test.step('opening broken code project should clear the scene and show the error', async () => { |     await test.step('opening broken code project should clear the scene and show the error', async () => { | ||||||
|       // Go back home. |       // Go back home. | ||||||
| @ -199,7 +199,7 @@ test( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| 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' }, |   { tag: '@electron' }, | ||||||
|   async ({ context, page }, testInfo) => { |   async ({ context, page }, testInfo) => { | ||||||
|     await context.folderSetupFn(async (dir) => { |     await context.folderSetupFn(async (dir) => { | ||||||
| @ -253,7 +253,7 @@ test( | |||||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() |       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||||
|       await expect(page.getByText('empty')).toBeVisible() |       await expect(page.getByText('empty')).toBeVisible() | ||||||
|       await expect(page.getByText('bracket')).toBeVisible() |       await expect(page.getByText('bracket')).toBeVisible() | ||||||
|       await expect(page.getByText('New Project')).toBeVisible() |       await expect(page.getByText('Create project')).toBeVisible() | ||||||
|     }) |     }) | ||||||
|     await test.step('opening empty code project should clear the scene', async () => { |     await test.step('opening empty code project should clear the scene', async () => { | ||||||
|       // Go back home. |       // Go back home. | ||||||
| @ -276,7 +276,7 @@ test( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| 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' }, |   { tag: '@electron' }, | ||||||
|   async ({ context, page }, testInfo) => { |   async ({ context, page }, testInfo) => { | ||||||
|     await context.folderSetupFn(async (dir) => { |     await context.folderSetupFn(async (dir) => { | ||||||
| @ -985,6 +985,126 @@ test.describe(`Project management commands`, () => { | |||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
|  |   test(`Create a new project with a colliding name`, async ({ | ||||||
|  |     context, | ||||||
|  |     homePage, | ||||||
|  |     toolbar, | ||||||
|  |     cmdBar, | ||||||
|  |   }) => { | ||||||
|  |     const projectName = 'test-project' | ||||||
|  |     await test.step(`Setup`, async () => { | ||||||
|  |       await context.folderSetupFn(async (dir) => { | ||||||
|  |         const projectDir = path.join(dir, projectName) | ||||||
|  |         await Promise.all([fsp.mkdir(projectDir, { recursive: true })]) | ||||||
|  |         await Promise.all([ | ||||||
|  |           fsp.copyFile( | ||||||
|  |             executorInputPath('router-template-slate.kcl'), | ||||||
|  |             path.join(projectDir, 'main.kcl') | ||||||
|  |           ), | ||||||
|  |         ]) | ||||||
|  |       }) | ||||||
|  |       await homePage.expectState({ | ||||||
|  |         projectCards: [ | ||||||
|  |           { | ||||||
|  |             title: projectName, | ||||||
|  |             fileCount: 1, | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |         sortBy: 'last-modified-desc', | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     await test.step('Create a new project with the same name', async () => { | ||||||
|  |       await cmdBar.openCmdBar() | ||||||
|  |       await cmdBar.chooseCommand('create project') | ||||||
|  |       await cmdBar.expectState({ | ||||||
|  |         stage: 'arguments', | ||||||
|  |         commandName: 'Create project', | ||||||
|  |         currentArgKey: 'name', | ||||||
|  |         currentArgValue: '', | ||||||
|  |         headerArguments: { | ||||||
|  |           Name: '', | ||||||
|  |         }, | ||||||
|  |         highlightedHeaderArg: 'name', | ||||||
|  |       }) | ||||||
|  |       await cmdBar.argumentInput.fill(projectName) | ||||||
|  |       await cmdBar.progressCmdBar() | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     await test.step(`Check the project was created with a non-colliding name`, async () => { | ||||||
|  |       await toolbar.logoLink.click() | ||||||
|  |       await homePage.expectState({ | ||||||
|  |         projectCards: [ | ||||||
|  |           { | ||||||
|  |             title: projectName + '-1', | ||||||
|  |             fileCount: 1, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             title: projectName, | ||||||
|  |             fileCount: 1, | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |         sortBy: 'last-modified-desc', | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     await test.step('Create another project with the same name', async () => { | ||||||
|  |       await cmdBar.openCmdBar() | ||||||
|  |       await cmdBar.chooseCommand('create project') | ||||||
|  |       await cmdBar.expectState({ | ||||||
|  |         stage: 'arguments', | ||||||
|  |         commandName: 'Create project', | ||||||
|  |         currentArgKey: 'name', | ||||||
|  |         currentArgValue: '', | ||||||
|  |         headerArguments: { | ||||||
|  |           Name: '', | ||||||
|  |         }, | ||||||
|  |         highlightedHeaderArg: 'name', | ||||||
|  |       }) | ||||||
|  |       await cmdBar.argumentInput.fill(projectName) | ||||||
|  |       await cmdBar.progressCmdBar() | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     await test.step(`Check the second project was created with a non-colliding name`, async () => { | ||||||
|  |       await toolbar.logoLink.click() | ||||||
|  |       await homePage.expectState({ | ||||||
|  |         projectCards: [ | ||||||
|  |           { | ||||||
|  |             title: projectName + '-2', | ||||||
|  |             fileCount: 1, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             title: projectName + '-1', | ||||||
|  |             fileCount: 1, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             title: projectName, | ||||||
|  |             fileCount: 1, | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |         sortBy: 'last-modified-desc', | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | test(`Create a few projects using the default project name`, async ({ | ||||||
|  |   homePage, | ||||||
|  |   toolbar, | ||||||
|  | }) => { | ||||||
|  |   for (let i = 0; i < 12; i++) { | ||||||
|  |     await test.step(`Create project ${i}`, async () => { | ||||||
|  |       await homePage.expectState({ | ||||||
|  |         projectCards: Array.from({ length: i }, (_, i) => ({ | ||||||
|  |           title: `project-${i.toString().padStart(3, '0')}`, | ||||||
|  |           fileCount: 1, | ||||||
|  |         })).toReversed(), | ||||||
|  |         sortBy: 'last-modified-desc', | ||||||
|  |       }) | ||||||
|  |       await homePage.createAndGoToProject() | ||||||
|  |       await toolbar.logoLink.click() | ||||||
|  |     }) | ||||||
|  |   } | ||||||
| }) | }) | ||||||
|  |  | ||||||
| test( | test( | ||||||
| @ -1391,7 +1511,7 @@ extrude001 = extrude(200, sketch001)`) | |||||||
|     await page.getByTestId('app-logo').click() |     await page.getByTestId('app-logo').click() | ||||||
|  |  | ||||||
|     await expect( |     await expect( | ||||||
|       page.getByRole('button', { name: 'New project' }) |       page.getByRole('button', { name: 'Create project' }) | ||||||
|     ).toBeVisible() |     ).toBeVisible() | ||||||
|  |  | ||||||
|     for (let i = 1; i <= 10; i++) { |     for (let i = 1; i <= 10; i++) { | ||||||
| @ -1465,7 +1585,7 @@ test( | |||||||
|  |  | ||||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() |       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||||
|       await expect(page.getByText('router-template-slate')).toBeVisible() |       await expect(page.getByText('router-template-slate')).toBeVisible() | ||||||
|       await expect(page.getByText('New Project')).toBeVisible() |       await expect(page.getByText('Create project')).toBeVisible() | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await test.step('Opening the router-template project should load the stream', async () => { |     await test.step('Opening the router-template project should load the stream', async () => { | ||||||
| @ -1494,7 +1614,7 @@ test( | |||||||
|  |  | ||||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() |       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||||
|       await expect(page.getByText('router-template-slate')).toBeVisible() |       await expect(page.getByText('router-template-slate')).toBeVisible() | ||||||
|       await expect(page.getByText('New Project')).toBeVisible() |       await expect(page.getByText('Create project')).toBeVisible() | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| ) | ) | ||||||
| @ -1885,3 +2005,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() | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
| @ -614,6 +614,38 @@ extrude001 = extrude(50, sketch001) | |||||||
|       await expect(gizmo).toBeVisible() |       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) { | async function clickExportButton(page: Page) { | ||||||
|  | |||||||
| @ -39,8 +39,8 @@ test.describe('Sketch tests', () => { | |||||||
|   ${startProfileAt1} |   ${startProfileAt1} | ||||||
|   |> arc({ |   |> arc({ | ||||||
|         radius = screwRadius, |         radius = screwRadius, | ||||||
|         angle_start = 0, |         angleStart = 0, | ||||||
|         angle_end = 360 |         angleEnd = 360 | ||||||
|       }, %) |       }, %) | ||||||
|    |    | ||||||
|     part001 = startSketchOn('XY') |     part001 = startSketchOn('XY') | ||||||
| @ -60,8 +60,8 @@ test.describe('Sketch tests', () => { | |||||||
|   |> yLine(wireOffset, %) |   |> yLine(wireOffset, %) | ||||||
|   |> arc({ |   |> arc({ | ||||||
|         radius = wireRadius, |         radius = wireRadius, | ||||||
|         angle_start = 0, |         angleStart = 0, | ||||||
|         angle_end = 180 |         angleEnd = 180 | ||||||
|       }, %) |       }, %) | ||||||
|   |> yLine(-wireOffset, %) |   |> yLine(-wireOffset, %) | ||||||
|   |> xLine(-width / 4, %) |   |> xLine(-width / 4, %) | ||||||
| @ -82,19 +82,16 @@ test.describe('Sketch tests', () => { | |||||||
|     await u.closeDebugPanel() |     await u.closeDebugPanel() | ||||||
|  |  | ||||||
|     await page.getByText(selectionsSnippets.startProfileAt1).click() |     await page.getByText(selectionsSnippets.startProfileAt1).click() | ||||||
|     await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() |  | ||||||
|     await expect( |     await expect( | ||||||
|       page.getByRole('button', { name: 'Edit Sketch' }) |       page.getByRole('button', { name: 'Edit Sketch' }) | ||||||
|     ).toBeVisible() |     ).toBeVisible() | ||||||
|  |  | ||||||
|     await page.getByText(selectionsSnippets.startProfileAt2).click() |     await page.getByText(selectionsSnippets.startProfileAt2).click() | ||||||
|     await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() |  | ||||||
|     await expect( |     await expect( | ||||||
|       page.getByRole('button', { name: 'Edit Sketch' }) |       page.getByRole('button', { name: 'Edit Sketch' }) | ||||||
|     ).toBeVisible() |     ).toBeVisible() | ||||||
|  |  | ||||||
|     await page.getByText(selectionsSnippets.startProfileAt3).click() |     await page.getByText(selectionsSnippets.startProfileAt3).click() | ||||||
|     await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled() |  | ||||||
|     await expect( |     await expect( | ||||||
|       page.getByRole('button', { name: 'Edit Sketch' }) |       page.getByRole('button', { name: 'Edit Sketch' }) | ||||||
|     ).toBeVisible() |     ).toBeVisible() | ||||||
| @ -1326,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 u.closeKclCodePanel() | ||||||
|   await expect(page).toHaveScreenshot({ |   await expect(page).toHaveScreenshot({ | ||||||
|     maxDiffPixels: 100, |     maxDiffPixels: 100, | ||||||
|  |     mask: [page.getByTestId('model-state-indicator')], | ||||||
|   }) |   }) | ||||||
|   await u.openKclCodePanel() |   await u.openKclCodePanel() | ||||||
| } | } | ||||||
|  | |||||||
| 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: 60 KiB | 
| Before Width: | Height: | Size: 55 KiB | 
| Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 48 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: 44 KiB | 
| Before Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 34 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 | 
| 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: 52 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 | 
| Before Width: | Height: | Size: 148 KiB | 
| Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 145 KiB | 
| Before Width: | Height: | Size: 130 KiB | 
| Before Width: | Height: | Size: 132 KiB | 
| Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 129 KiB | 
| Before Width: | Height: | Size: 111 KiB | 
| Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 37 KiB | 
