Compare commits
	
		
			150 Commits
		
	
	
		
			nrc-test-m
			...
			franknoiro
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e3400705f4 | |||
| 10ed312b28 | |||
| da9cae98aa | |||
| 9ae025dc56 | |||
| 579ab23d78 | |||
| 4bef33e745 | |||
| ac7bd28c5a | |||
| d478d81156 | |||
| 3d27f0191b | |||
| 30c2acd18a | |||
| a83b4b2145 | |||
| 70b8541038 | |||
| bb51646738 | |||
| c02e31a530 | |||
| 1d06cc7845 | |||
| e0c07eecfe | |||
| c5d42500fa | |||
| e6e47f77f0 | |||
| 662c2485ac | |||
| 9f891deebb | |||
| d08a07a1f8 | |||
| 872b196a86 | |||
| d535a2862d | |||
| 63a3bc7bc6 | |||
| 02055a8b31 | |||
| 93891422f7 | |||
| 7193b4110a | |||
| 76e7d80a55 | |||
| b816df21d2 | |||
| 3630696848 | |||
| f165d19fda | |||
| 3dd98ae1d5 | |||
| a46e0a0fe7 | |||
| 8f9dc06228 | |||
| fa22c14723 | |||
| 1d39983b08 | |||
| da301ba862 | |||
| efe8089b08 | |||
| 49de3b0ac9 | |||
| 2b2ed470c1 | |||
| 96652a0c48 | |||
| 04e586d07b | |||
| fe5f574a77 | |||
| e787495ad0 | |||
| 8bb9be7a5e | |||
| 00892464e8 | |||
| 05ed2a3367 | |||
| 10cc5bce59 | |||
| a32f150fc1 | |||
| ac60082e67 | |||
| d44dc1b21a | |||
| 813962ea4c | |||
| 738443a6ab | |||
| 4b6bbbe2c5 | |||
| 6ff8addc8b | |||
| da05c38b9e | |||
| 191b9b71fd | |||
| 05163fdded | |||
| 7ed26e21c6 | |||
| c668d40efc | |||
| f38c6b90b7 | |||
| 7bc8bae0ec | |||
| 3804aca27e | |||
| b127680f2f | |||
| b7de8e60cf | |||
| 058fccb5e1 | |||
| 00e97257ae | |||
| aeb656d176 | |||
| ac49ebd6e0 | |||
| b40f03ad25 | |||
| a8ad86e645 | |||
| 87f50cd5e9 | |||
| 0400e6228e | |||
| 26f150fd6c | |||
| 3049f405f5 | |||
| 53d40301dc | |||
| 671c01e36f | |||
| e80151979b | |||
| 668e2afb99 | |||
| 548c664db0 | |||
| d3a3f4410c | |||
| 22eb343171 | |||
| f2cfa4d5cf | |||
| 3f1f40eeba | |||
| ff2d161606 | |||
| 210c78029d | |||
| e27840219b | |||
| c943a3f192 | |||
| 6aa588f09f | |||
| 59a6333aad | |||
| 403f1507ae | |||
| eac7b83504 | |||
| 667500d1b9 | |||
| b15aac9f48 | |||
| 54153aa646 | |||
| 943cf21d34 | |||
| 5a6728c45a | |||
| ff2103d493 | |||
| 2dfa8f2176 | |||
| 29ed330326 | |||
| ca2cc825a6 | |||
| 83fe1b7ce0 | |||
| 157b76cc78 | |||
| cf957d880e | |||
| dfc3d19677 | |||
| dd370a9365 | |||
| 2274d6459c | |||
| 32ce857119 | |||
| 88b51da417 | |||
| 30d365aeb3 | |||
| 7af62399ac | |||
| 441d957228 | |||
| 9e57034873 | |||
| eb96d6539c | |||
| 513c76ecc8 | |||
| 51d9449280 | |||
| 6366bc4766 | |||
| 7a21918223 | |||
| 8072f1db63 | |||
| 18e1855fa9 | |||
| 7be53c7d4a | |||
| 2bf20988ef | |||
| 1495cc6d18 | |||
| f876e6ca3c | |||
| 60a0c811ab | |||
| cab0c1e6a1 | |||
| 417d720b22 | |||
| 77293952c0 | |||
| ea3d604b73 | |||
| 023a659491 | |||
| dd3a2b14f9 | |||
| 424b409cc1 | |||
| 82a58e69c2 | |||
| 776b420031 | |||
| 1087d4223b | |||
| 089d6df889 | |||
| efb067af58 | |||
| 2aa27eab01 | |||
| 9c47ac5b57 | |||
| 5ae1aecd74 | |||
| 68ae7e98f9 | |||
| 56771d561a | |||
| f09411817c | |||
| bed7ae3b8b | |||
| c43510732c | |||
| 51f0b669a4 | |||
| 3cbedcd3e7 | |||
| 5d2fa43150 | |||
| ec49b0752e | |||
| 3b171fb881 | 
| @ -1,59 +0,0 @@ | ||||
| # bash strict mode | ||||
| set -euo pipefail | ||||
|  | ||||
| if [[ ! -f "test-results/.last-run.json" ]]; then | ||||
|     # if no last run artifact, than run plawright normally | ||||
|     echo "run playwright normally" | ||||
|     if [[ "$3" == ubuntu-latest* ]]; then | ||||
|         yarn test:playwright:browser:chrome:ubuntu -- --shard=$1/$2 || true | ||||
|     elif [[ "$3" == windows-latest* ]]; then | ||||
|         yarn test:playwright:browser:chrome:windows -- --shard=$1/$2 || true | ||||
|     else | ||||
|         echo "Do not run playwright. Unable to detect os runtime." | ||||
|         exit 1 | ||||
|     fi | ||||
|     # # send to axiom | ||||
|     node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1 | ||||
| fi | ||||
|  | ||||
| retry=1 | ||||
| max_retrys=4 | ||||
|  | ||||
| # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues | ||||
| while [[ $retry -le $max_retrys ]]; do | ||||
|     if [[ -f "test-results/.last-run.json" ]]; then | ||||
|         failed_tests=$(jq '.failedTests | length' test-results/.last-run.json) | ||||
|         if [[ $failed_tests -gt 0 ]]; then | ||||
|             echo "retried=true" >>$GITHUB_OUTPUT | ||||
|             echo "run playwright with last failed tests and retry $retry" | ||||
|             if [[ "$3" == ubuntu-latest* ]]; then | ||||
|                 yarn test:playwright:browser:chrome:ubuntu -- --last-failed || true | ||||
|             elif [[ "$3" == windows-latest* ]]; then | ||||
|                 yarn test:playwright:browser:chrome:windows -- --last-failed || true | ||||
|             else | ||||
|                 echo "Do not run playwright. Unable to detect os runtime." | ||||
|                 exit 1 | ||||
|             fi | ||||
|             # send to axiom | ||||
|             node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1 | ||||
|             retry=$((retry + 1)) | ||||
|         else | ||||
|             echo "retried=false" >>$GITHUB_OUTPUT | ||||
|             exit 0 | ||||
|         fi | ||||
|     else | ||||
|         echo "retried=false" >>$GITHUB_OUTPUT | ||||
|         exit 0 | ||||
|     fi | ||||
| done | ||||
|  | ||||
| echo "retried=false" >>$GITHUB_OUTPUT | ||||
|  | ||||
| if [[ -f "test-results/.last-run.json" ]]; then | ||||
|     failed_tests=$(jq '.failedTests | length' test-results/.last-run.json) | ||||
|     if [[ $failed_tests -gt 0 ]]; then | ||||
|         # if it still fails after 3 retrys, then fail the job | ||||
|         exit 1 | ||||
|     fi | ||||
| fi | ||||
| exit 0 | ||||
							
								
								
									
										20
									
								
								.github/ci-cd-scripts/playwright-electron.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -1,15 +1,17 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| # bash strict mode | ||||
| set -euo pipefail | ||||
|  | ||||
| if [[ ! -f "test-results/.last-run.json" ]]; then | ||||
|     # if no last run artifact, than run plawright normally | ||||
|     echo "run playwright normally" | ||||
|         if [[ "$1" == ubuntu-latest* ]]; then | ||||
|             xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu || true | ||||
|         elif [[ "$1" == windows-latest* ]]; then | ||||
|             yarn test:playwright:electron:windows || true | ||||
|         elif [[ "$1" == macos-14* ]]; then | ||||
|             yarn test:playwright:electron:macos || true | ||||
|         if [[ "$3" == *ubuntu* ]]; then | ||||
|             xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --shard=$1/$2 || true | ||||
|         elif [[ "$3" == *windows* ]]; then | ||||
|             yarn test:playwright:electron:windows -- --shard=$1/$2 || true | ||||
|         elif [[ "$3" == *macos* ]]; then | ||||
|             yarn test:playwright:electron:macos  -- --shard=$1/$2 || true | ||||
|         else | ||||
|             echo "Do not run playwright. Unable to detect os runtime." | ||||
|             exit 1 | ||||
| @ -28,11 +30,11 @@ while [[ $retry -le $max_retrys ]]; do | ||||
|         if [[ $failed_tests -gt 0 ]]; then | ||||
|             echo "retried=true" >>$GITHUB_OUTPUT | ||||
|             echo "run playwright with last failed tests and retry $retry" | ||||
|             if [[ "$1" == ubuntu-latest* ]]; then | ||||
|             if [[ "$3" == *ubuntu* ]]; then | ||||
|                 xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --last-failed || true | ||||
|             elif [[ "$1" == windows-latest* ]]; then | ||||
|             elif [[ "$3" == *windows* ]]; then | ||||
|                 yarn test:playwright:electron:windows -- --last-failed || true | ||||
|             elif [[ "$1" == macos-14* ]]; then | ||||
|             elif [[ "$3" == *macos* ]]; then | ||||
|                 yarn test:playwright:electron:macos -- --last-failed || true | ||||
|             else | ||||
|                 echo "Do not run playwright. Unable to detect os runtime." | ||||
|  | ||||
							
								
								
									
										51
									
								
								.github/workflows/build-apps.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -165,7 +165,6 @@ jobs: | ||||
|       - name: Build the app (release) | ||||
|         if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }} | ||||
|         env: | ||||
|           PUBLISH_FOR_PULL_REQUEST: true | ||||
|           APPLE_ID: ${{ secrets.APPLE_ID }} | ||||
|           APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | ||||
|           APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | ||||
| @ -173,9 +172,14 @@ jobs: | ||||
|           CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} | ||||
|           CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | ||||
|           CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||||
|           CSC_FOR_PULL_REQUEST: true | ||||
|           WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} | ||||
|         run: yarn electron-builder --config --publish always | ||||
|           DEBUG: "electron-notarize*" | ||||
|         # TODO: Fix electron-notarize flakes. The logs above should help gather more data on failures | ||||
|         uses: nick-fields/retry@v3.0.0 | ||||
|         with: | ||||
|           timeout_minutes: 10 | ||||
|           max_attempts: 3 | ||||
|           command: yarn electron-builder --config --publish always | ||||
|  | ||||
|       - name: List artifacts in out/ | ||||
|         run: ls -R out | ||||
| @ -229,9 +233,14 @@ jobs: | ||||
|           CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} | ||||
|           CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | ||||
|           CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||||
|           CSC_FOR_PULL_REQUEST: true | ||||
|           WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} | ||||
|         run: yarn electron-builder --config --publish always | ||||
|           DEBUG: "electron-notarize*" | ||||
|         # TODO: Fix electron-notarize flakes. The logs above should help gather more data on failures | ||||
|         uses: nick-fields/retry@v3.0.0 | ||||
|         with: | ||||
|           timeout_minutes: 10 | ||||
|           max_attempts: 3 | ||||
|           command: yarn electron-builder --config --publish always | ||||
|  | ||||
|       - uses: actions/upload-artifact@v4 | ||||
|         if: ${{ env.IS_RELEASE == 'true' }} | ||||
| @ -362,6 +371,17 @@ jobs: | ||||
|       - name: List artifacts | ||||
|         run: "ls -R out" | ||||
|  | ||||
|       - name: Set more complete nightly release notes | ||||
|         if: ${{ env.IS_NIGHTLY == 'true' }} | ||||
|         run: | | ||||
|           # Note: preferred going this way instead of a full clone in the checkout step, | ||||
|           # see https://github.com/actions/checkout/issues/1471 | ||||
|           git fetch --prune --unshallow --tags | ||||
|           export TAG="nightly-${VERSION}" | ||||
|           export PREVIOUS_TAG=$(git describe --tags --match="nightly-v[0-9]*" --abbrev=0) | ||||
|           export NOTES=$(./scripts/get-nightly-changelog.sh) | ||||
|           yarn files:set-notes | ||||
|  | ||||
|       - name: Authenticate to Google Cloud | ||||
|         if: ${{ env.IS_NIGHTLY == 'true' }} | ||||
|         uses: 'google-github-actions/auth@v2.1.7' | ||||
| @ -383,12 +403,17 @@ jobs: | ||||
|           parent: false | ||||
|           destination: 'dl.kittycad.io/releases/modeling-app/nightly' | ||||
|  | ||||
|       - name: Create draft release | ||||
|         uses: softprops/action-gh-release@v2 | ||||
|         if: ${{ env.IS_RELEASE == 'true' }} | ||||
|       - name: Invalidate bucket cache on latest*.yml and last_download.json files | ||||
|         if: ${{ env.IS_NIGHTLY == 'true' }} | ||||
|         run: yarn files:invalidate-bucket:nightly | ||||
|  | ||||
|       - name: Tag nightly commit | ||||
|         if: ${{ env.IS_NIGHTLY == 'true' }} | ||||
|         uses: actions/github-script@v7 | ||||
|         with: | ||||
|           name: ${{ env.VERSION }} | ||||
|           tag_name: ${{ env.VERSION }} | ||||
|           draft: true | ||||
|           generate_release_notes: true | ||||
|           files: 'out/Zoo*' | ||||
|           script: | | ||||
|             const { VERSION } = process.env           | ||||
|             const { owner, repo } = context.repo | ||||
|             const { sha } = context | ||||
|             const ref = `refs/tags/nightly-${VERSION}` | ||||
|             github.rest.git.createRef({ owner, repo, sha, ref }) | ||||
|  | ||||
							
								
								
									
										22
									
								
								.github/workflows/cargo-test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -2,28 +2,8 @@ on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - 'src/wasm-lib/**.rs' | ||||
|       - 'src/wasm-lib/**.hbs' | ||||
|       - 'src/wasm-lib/**.gen' | ||||
|       - 'src/wasm-lib/**.snap' | ||||
|       - '**/Cargo.toml' | ||||
|       - '**/Cargo.lock' | ||||
|       - '**/rust-toolchain.toml' | ||||
|       - 'src/wasm-lib/**.kcl' | ||||
|       - .github/workflows/cargo-test.yml | ||||
|  | ||||
|   pull_request: | ||||
|     paths: | ||||
|       - 'src/wasm-lib/**.rs' | ||||
|       - 'src/wasm-lib/**.hbs' | ||||
|       - 'src/wasm-lib/**.gen' | ||||
|       - 'src/wasm-lib/**.snap' | ||||
|       - '**/Cargo.toml' | ||||
|       - '**/Cargo.lock' | ||||
|       - '**/rust-toolchain.toml' | ||||
|       - 'src/wasm-lib/**.kcl' | ||||
|       - .github/workflows/cargo-test.yml | ||||
|   workflow_dispatch: | ||||
| permissions: read-all | ||||
| concurrency: | ||||
| @ -71,7 +51,7 @@ jobs: | ||||
|           KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}} | ||||
|           RUST_MIN_STACK: 10485760000 | ||||
|       - name: Upload to codecov.io | ||||
|         uses: codecov/codecov-action@v4 | ||||
|         uses: codecov/codecov-action@v5 | ||||
|         with: | ||||
|           token: ${{secrets.CODECOV_TOKEN}} | ||||
|           fail_ci_if_error: true | ||||
|  | ||||
							
								
								
									
										166
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -18,6 +18,7 @@ permissions: | ||||
| jobs: | ||||
|  | ||||
|   check-rust-changes: | ||||
|     if: github.event.pull_request.draft == false | ||||
|     runs-on: ubuntu-latest | ||||
|     outputs: | ||||
|       rust-changed: ${{ steps.filter.outputs.rust }} | ||||
| @ -33,20 +34,20 @@ jobs: | ||||
|             rust: | ||||
|               - 'src/wasm-lib/**' | ||||
|  | ||||
|   browser: | ||||
|     timeout-minutes: ${{ matrix.os == 'macos-14' && 60 || 50 }} | ||||
|     name: playwright:browser:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} | ||||
|   electron: | ||||
|     if: github.event.pull_request.draft == false | ||||
|     timeout-minutes: 60 | ||||
|     name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         os: [ubuntu-latest-8-cores, windows-latest-8-cores] | ||||
|         # TODO: enable self-hosted-windows-8-cores once available | ||||
|         os: [namespace-profile-ubuntu-8-cores, namespace-profile-macos-8-cores, windows-16-cores] | ||||
|         shardIndex: [1, 2, 3, 4] | ||||
|         shardTotal: [4] | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     needs: check-rust-changes | ||||
|     steps: | ||||
|     - name: Tune GitHub-hosted runner network | ||||
|       uses: smorimoto/tune-github-hosted-runner-network@v1 | ||||
|     - uses: actions/checkout@v4 | ||||
|     - uses: actions/setup-node@v4 | ||||
|       with: | ||||
| @ -68,7 +69,7 @@ jobs: | ||||
|     - name: Download Wasm Cache | ||||
|       id: download-wasm | ||||
|       if: needs.check-rust-changes.outputs.rust-changed == 'false' | ||||
|       uses: dawidd6/action-download-artifact@v6 | ||||
|       uses: dawidd6/action-download-artifact@v7 | ||||
|       continue-on-error: true | ||||
|       with: | ||||
|         github_token: ${{secrets.GITHUB_TOKEN}} | ||||
| @ -101,7 +102,8 @@ jobs: | ||||
|         echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH | ||||
|     - name: Install vector | ||||
|       shell: bash | ||||
|       if:  ${{ !startsWith(matrix.os, 'windows') }} | ||||
|       # TODO: figure out what to do with this, it's failing | ||||
|       if: false | ||||
|       run: | | ||||
|         curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh | ||||
|         chmod +x /tmp/vector.sh | ||||
| @ -123,13 +125,13 @@ jobs: | ||||
|       if: steps.download-wasm.outcome == 'failure' | ||||
|       shell: bash | ||||
|       run: yarn build:wasm | ||||
|     - name: build web | ||||
|       run: yarn build:local | ||||
|     - name: build electron | ||||
|       shell: bash | ||||
|       run: yarn tron:package | ||||
|     - name: Run ubuntu/chrome snapshots | ||||
|       shell: bash | ||||
|       run: | | ||||
|         yarn playwright test --project="Google Chrome" --config=playwright.ci.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=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} | ||||
|       env: | ||||
|         CI: true | ||||
|         NODE_ENV: development | ||||
| @ -186,12 +188,12 @@ jobs: | ||||
|       with: | ||||
|         name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         path: test-results/ | ||||
|     - name: Run playwright/chrome flow (with retries) | ||||
|     - name: Run playwright/electron flow (with retries) | ||||
|       id: retry | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       shell: bash | ||||
|       run: | | ||||
|         .github/ci-cd-scripts/playwright-browser-chrome.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{matrix.os}} | ||||
|         .github/ci-cd-scripts/playwright-electron.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{matrix.os}} | ||||
|       env: | ||||
|         CI: true | ||||
|         FAIL_ON_CONSOLE_ERRORS: true | ||||
| @ -199,11 +201,6 @@ jobs: | ||||
|         VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} | ||||
|         VITE_KC_SKIP_AUTH: true | ||||
|         token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} | ||||
|     - name: send to axiom | ||||
|       if: always() | ||||
|       shell: bash | ||||
|       run: | | ||||
|         node playwrightProcess.mjs | tee /tmp/github-actions.log | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: always() | ||||
|       with: | ||||
| @ -221,136 +218,3 @@ jobs: | ||||
|         retention-days: 30 | ||||
|         overwrite: true | ||||
|  | ||||
|  | ||||
|   electron: | ||||
|     name: playwright:electron:${{matrix.os}} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         os: [ubuntu-latest-8-cores, windows-latest-8-cores, macos-14-large] | ||||
|     timeout-minutes: 60 | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     needs: check-rust-changes | ||||
|     steps: | ||||
|     - name: Tune GitHub-hosted runner network | ||||
|       uses: smorimoto/tune-github-hosted-runner-network@v1 | ||||
|     - uses: actions/checkout@v4 | ||||
|     - uses: actions/setup-node@v4 | ||||
|       with: | ||||
|         node-version-file: '.nvmrc' | ||||
|         cache: 'yarn' | ||||
|     - uses: KittyCAD/action-install-cli@main | ||||
|     - name: Install dependencies | ||||
|       shell: bash | ||||
|       run: yarn | ||||
|     - name: Cache Playwright Browsers | ||||
|       uses: actions/cache@v4 | ||||
|       with: | ||||
|         path: | | ||||
|           ~/.cache/ms-playwright/ | ||||
|         key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }} | ||||
|     - name: Install Playwright Browsers | ||||
|       shell: bash | ||||
|       run: yarn playwright install chromium --with-deps | ||||
|     - name: Download Wasm Cache | ||||
|       id: download-wasm | ||||
|       if: needs.check-rust-changes.outputs.rust-changed == 'false' | ||||
|       uses: dawidd6/action-download-artifact@v6 | ||||
|       continue-on-error: true | ||||
|       with: | ||||
|         github_token: ${{secrets.GITHUB_TOKEN}} | ||||
|         name: wasm-bundle | ||||
|         workflow: build-and-store-wasm.yml | ||||
|         branch: main | ||||
|         path: src/wasm-lib/pkg | ||||
|     - name: copy wasm blob | ||||
|       if: needs.check-rust-changes.outputs.rust-changed == 'false' | ||||
|       shell: bash | ||||
|       run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public | ||||
|       continue-on-error: true | ||||
|     - name: Setup Rust | ||||
|       uses: dtolnay/rust-toolchain@stable | ||||
|     - name: Cache Wasm (because rust diff) | ||||
|       if: needs.check-rust-changes.outputs.rust-changed == 'true' | ||||
|       uses: Swatinem/rust-cache@v2 | ||||
|       with: | ||||
|         workspaces: './src/wasm-lib' | ||||
|     - name: OR Cache Wasm (because wasm cache failed) | ||||
|       if: steps.download-wasm.outcome == 'failure' | ||||
|       uses: Swatinem/rust-cache@v2 | ||||
|       with: | ||||
|         workspaces: './src/wasm-lib' | ||||
|     - name: install good sed | ||||
|       if:  ${{ startsWith(matrix.os, 'macos') }} | ||||
|       shell: bash | ||||
|       run: | | ||||
|         brew install gnu-sed | ||||
|         echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH | ||||
|     - name: Install vector | ||||
|       if:  ${{ startsWith(matrix.os, 'ubuntu') }} | ||||
|       shell: bash | ||||
|       run: | | ||||
|         curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh | ||||
|         chmod +x /tmp/vector.sh | ||||
|         /tmp/vector.sh -y -no-modify-path | ||||
|         mkdir -p /tmp/vector | ||||
|         cp .github/workflows/vector.toml /tmp/vector.toml | ||||
|         sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml | ||||
|         sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml | ||||
|         sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml | ||||
|         sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml | ||||
|         sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml | ||||
|         cat /tmp/vector.toml | ||||
|         ${HOME}/.vector/bin/vector --config /tmp/vector.toml & | ||||
|     - name: Build Wasm (because rust diff) | ||||
|       if: needs.check-rust-changes.outputs.rust-changed == 'true' | ||||
|       shell: bash | ||||
|       run: yarn build:wasm | ||||
|     - name: OR Build Wasm (because wasm cache failed) | ||||
|       if: steps.download-wasm.outcome == 'failure' | ||||
|       shell: bash | ||||
|       run: yarn build:wasm | ||||
|     - name: build electron | ||||
|       shell: bash | ||||
|       run: yarn tron:package | ||||
|     - uses: actions/download-artifact@v4 | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       continue-on-error: true | ||||
|       with: | ||||
|         name: test-results-electron-${{ matrix.os }}-${{ github.sha }} | ||||
|         path: test-results/ | ||||
|     - name: Run electron tests (with retries) | ||||
|       id: retry | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       shell: bash | ||||
|       run: | | ||||
|         .github/ci-cd-scripts/playwright-electron.sh ${{ matrix.os }} | ||||
|       env: | ||||
|         CI: true | ||||
|         FAIL_ON_CONSOLE_ERRORS: true | ||||
|         NODE_ENV: development | ||||
|         VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} | ||||
|         VITE_KC_SKIP_AUTH: true | ||||
|         IS_UBUNTU: ${{ startsWith(matrix.os, 'ubuntu') && 'true' || 'false' }} | ||||
|         #DEBUG: 'pw:browser*' | ||||
|     - name: send to axiom | ||||
|       if: ${{ !cancelled() && (success() || failure()) && !startsWith(matrix.os, 'windows') }} | ||||
|       shell: bash | ||||
|       run: | | ||||
|         node playwrightProcess.mjs | tee /tmp/github-actions.log | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       with: | ||||
|         name: test-results-electron-${{ matrix.os }}-${{ github.sha }} | ||||
|         path: test-results/ | ||||
|         include-hidden-files: true | ||||
|         retention-days: 30 | ||||
|         overwrite: true | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       with: | ||||
|         name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }} | ||||
|         path: playwright-report/ | ||||
|         include-hidden-files: true | ||||
|         retention-days: 30 | ||||
|         overwrite: true | ||||
|  | ||||
							
								
								
									
										12
									
								
								.github/workflows/publish-apps-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -126,11 +126,13 @@ jobs: | ||||
|           destination: 'dl.kittycad.io/releases/modeling-app' | ||||
|  | ||||
|       - name: Invalidate bucket cache on latest*.yml and last_download.json files | ||||
|         run: | | ||||
|           gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/last_download.json" --async | ||||
|           gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-linux-arm64.yml" --async | ||||
|           gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async | ||||
|           gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async | ||||
|         run: yarn files:invalidate-bucket | ||||
|  | ||||
|       - name: Upload release files to Github | ||||
|         if: ${{ github.event_name == 'release' }} | ||||
|         uses: softprops/action-gh-release@v2 | ||||
|         with: | ||||
|           files: 'out/Zoo*' | ||||
|  | ||||
|  | ||||
|   announce_release: | ||||
|  | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -61,6 +61,7 @@ Mac_App_Distribution.provisionprofile | ||||
| *.tsbuildinfo | ||||
| src/wasm-lib/pkg | ||||
|  | ||||
| .eslintcache | ||||
| venv | ||||
| .vite/ | ||||
|  | ||||
|  | ||||
							
								
								
									
										43
									
								
								INSTALL.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,43 @@ | ||||
| # Setting Up Zoo Modeling App | ||||
|  | ||||
| Compared to other CAD software, getting Zoo Modeling App up and running is quick and straightforward across platforms. It's about 100MB to download and is quick to install. | ||||
|  | ||||
| ## Windows | ||||
|  | ||||
| 1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for Windows and for your processor type. | ||||
|  | ||||
| 2. Once downloaded, run the installer `Zoo Modeling App-{version}-{arch}-win.exe` which should take a few seconds. | ||||
|  | ||||
| 3. The installation happens at `C:\Program Files\Zoo Modeling App`. A shortcut in the start menu is also created so you can run the app easily by clicking on it. | ||||
|  | ||||
| ## macOS | ||||
|  | ||||
| 1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for macOS and for your processor type. | ||||
|  | ||||
| 2. Once downloaded, open the disk image `Zoo Modeling App-{version}-{arch}-mac.dmg` and drag the applications to your `Applications` directory. | ||||
|  | ||||
| 3. You can then open your `Applications` directory and double-click on `Zoo Modeling App` to open. | ||||
|  | ||||
|  | ||||
| ## Linux  | ||||
|  | ||||
| 1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for Linux and for your processor type. | ||||
|  | ||||
| 2. Install the dependencies needed to run the [AppImage format](https://appimage.org/). | ||||
|     -  On Ubuntu, install the FUSE library with these commands in a terminal. | ||||
|        ```bash | ||||
|        sudo apt update | ||||
|        sudo apt install libfuse2 | ||||
|        ``` | ||||
|     - Optionally, follow [these steps](https://github.com/probonopd/go-appimage/blob/master/src/appimaged/README.md#initial-setup) to install `appimaged`. It is a daemon that makes interacting with AppImage files more seamless.  | ||||
|     - Once installed, copy the downloaded `Zoo Modeling App-{version}-{arch}-linux.AppImage` to the directory of your choice, for instance `~/Applications`. | ||||
|  | ||||
|    - `appimaged` should automatically find it and make it executable. If not, run: | ||||
|      ```bash | ||||
|      chmod a+x ~/Applications/Zoo\ Modeling\ App-{version}-{arch}-linux.AppImage | ||||
|      ``` | ||||
|  | ||||
| 3. You can double-click on the AppImage to run it, or in a terminal with this command: | ||||
|    ```bash | ||||
|     ~/Applications/Zoo\ Modeling\ App-{version}-{arch}-linux.AppImage | ||||
|    ``` | ||||
							
								
								
									
										33
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @ -99,7 +99,7 @@ yarn tron:start | ||||
|  | ||||
| This will start the application and hot-reload on changes. | ||||
|  | ||||
| Devtools can be opened with the usual Cmd/Ctrl-Shift-I. | ||||
| Devtools can be opened with the usual Cmd-Opt-I (Mac) or Ctrl-Shift-I (Linux and Windows). | ||||
|  | ||||
| To build, run `yarn tron:package`. | ||||
|  | ||||
| @ -136,7 +136,7 @@ https://github.com/KittyCAD/modeling-app/issues/new | ||||
|  | ||||
| #### 2. Push a new tag | ||||
|  | ||||
| Create a new tag and push it to the repo (eg. `v0.28.0` for `$VERSION`) | ||||
| Create a new tag and push it to the repo. The `semantic-release.sh` script will automatically bump the minor part, which we use the most. For instance going from `v0.27.0` to `v0.28.0`. | ||||
|  | ||||
| ``` | ||||
| VERSION=$(./scripts/semantic-release.sh) | ||||
| @ -146,16 +146,14 @@ git push origin --tags | ||||
|  | ||||
| This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files as well as updater-test artifacts. | ||||
|  | ||||
| Once the workflow succeeds, a draft release will be created at https://github.com/KittyCAD/modeling-app/releases. | ||||
| The workflow should be listed right away [in this list](https://github.com/KittyCAD/modeling-app/actions/workflows/build-apps.yml?query=event%3Apush)). | ||||
|  | ||||
| #### 3. Manually test artifacts from the Cut Release PR | ||||
| #### 3. Manually test artifacts | ||||
|  | ||||
| ##### Release builds | ||||
|  | ||||
| The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.). | ||||
|  | ||||
| Alternatively, the draft release will also include these builds. | ||||
|  | ||||
| Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue. | ||||
|  | ||||
| ##### Updater-test builds | ||||
| @ -178,9 +176,11 @@ If the prompt doesn't show up, start the app in command line to grab the electro | ||||
|  | ||||
| #### 4. Publish the release | ||||
|  | ||||
| Head over to https://github.com/KittyCAD/modeling-app/releases, paste in the changelog discussed in the issue, and publish the draft release created by the `build-apps` workflow from step 2. | ||||
| Head over to https://github.com/KittyCAD/modeling-app/releases/new, pick the newly created tag and type it in the _Release title_ field as well. | ||||
|  | ||||
| A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter. On success, the files will be uploaded to the public bucket and the announcement on Discord will be sent.  | ||||
| Hit _Generate release notes_ as a starting point to discuss the changelog in the issue. Once done, make sure _Set as the latest release_ is checked, and hit _Publish release_.  | ||||
|  | ||||
| A new `publish-apps-release` will kick in and you should be able to find it [here](https://github.com/KittyCAD/modeling-app/actions?query=event%3Arelease). On success, the files will be uploaded to the public bucket as well as to the GitHub release, and the announcement on Discord will be sent.  | ||||
|  | ||||
| #### 5. Close the issue | ||||
|  | ||||
| @ -388,23 +388,6 @@ yarn test:unit:local | ||||
|  | ||||
| #### E2E Tests | ||||
|  | ||||
| **Playwright Browser** | ||||
|  | ||||
| These E2E tests run in a browser (without electron). | ||||
| There are tests that are skipped if they are ran in a windows OS or Linux OS. We can use playwright tags to implement test skipping. | ||||
|  | ||||
| Breaking down the command `yarn test:playwright:browser:chrome:windows` | ||||
| - The application is `playwright` | ||||
| - The runtime is a `browser` | ||||
| - The specific `browser` is `chrome` | ||||
| - The test should run in a `windows` environment. It will skip tests that are broken or flaky in the windows OS. | ||||
|  | ||||
| ``` | ||||
| yarn test:playwright:browser:chrome | ||||
| yarn test:playwright:browser:chrome:windows | ||||
| yarn test:playwright:browser:chrome:ubuntu | ||||
| ``` | ||||
|  | ||||
| **Playwright Electron** | ||||
|  | ||||
| These E2E tests run in electron. There are tests that are skipped if they are ran in a windows, linux, or macos environment. We can use playwright tags to implement test skipping. | ||||
|  | ||||
| Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 119 KiB | 
| Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 259 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icon.ico
									
									
									
									
									
								
							
							
						
						| Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 114 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icon.png
									
									
									
									
									
								
							
							
						
						| Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB | 
| @ -22,3 +22,5 @@ once fixed in engine will just start working here with no language changes. | ||||
|  | ||||
| - **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple | ||||
|     chamfer cases work currently. | ||||
|  | ||||
| - **Appearance**: Changing the appearance on a loft does not work. | ||||
|  | ||||
							
								
								
									
										239
									
								
								docs/kcl/appearance.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										49
									
								
								docs/kcl/atan2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -19,6 +19,7 @@ layout: manual | ||||
| * [`angledLineThatIntersects`](kcl/angledLineThatIntersects) | ||||
| * [`angledLineToX`](kcl/angledLineToX) | ||||
| * [`angledLineToY`](kcl/angledLineToY) | ||||
| * [`appearance`](kcl/appearance) | ||||
| * [`arc`](kcl/arc) | ||||
| * [`arcTo`](kcl/arcTo) | ||||
| * [`asin`](kcl/asin) | ||||
| @ -29,6 +30,7 @@ layout: manual | ||||
| * [`assertLessThan`](kcl/assertLessThan) | ||||
| * [`assertLessThanOrEq`](kcl/assertLessThanOrEq) | ||||
| * [`atan`](kcl/atan) | ||||
| * [`atan2`](kcl/atan2) | ||||
| * [`bezierCurve`](kcl/bezierCurve) | ||||
| * [`ceil`](kcl/ceil) | ||||
| * [`chamfer`](kcl/chamfer) | ||||
| @ -99,8 +101,8 @@ layout: manual | ||||
| * [`sin`](kcl/sin) | ||||
| * [`sqrt`](kcl/sqrt) | ||||
| * [`startProfileAt`](kcl/startProfileAt) | ||||
| * [`startSketchAt`](kcl/startSketchAt) | ||||
| * [`startSketchOn`](kcl/startSketchOn) | ||||
| * [`sweep`](kcl/sweep) | ||||
| * [`tan`](kcl/tan) | ||||
| * [`tangentToEnd`](kcl/tangentToEnd) | ||||
| * [`tangentialArc`](kcl/tangentialArc) | ||||
|  | ||||
| @ -45,7 +45,7 @@ circles = map([1..3], drawCircle) | ||||
| ```js | ||||
| r = 10 // radius | ||||
| // Call `map`, using an anonymous function instead of a named one. | ||||
| circles = map([1..3], (id) { | ||||
| circles = map([1..3], fn(id) { | ||||
|   return startSketchOn("XY") | ||||
|     |> circle({ center = [id * 2 * r, 0], radius = r }, %) | ||||
| }) | ||||
|  | ||||
| @ -12,7 +12,7 @@ to other modules. | ||||
|  | ||||
| ``` | ||||
| // util.kcl | ||||
| export fn increment = (x) => { | ||||
| export fn increment(x) { | ||||
|   return x + 1 | ||||
| } | ||||
| ``` | ||||
| @ -37,11 +37,11 @@ Multiple functions can be exported in a file. | ||||
|  | ||||
| ``` | ||||
| // util.kcl | ||||
| export fn increment = (x) => { | ||||
| export fn increment(x) { | ||||
|   return x + 1 | ||||
| } | ||||
|  | ||||
| export fn decrement = (x) => { | ||||
| export fn decrement(x) { | ||||
|   return x - 1 | ||||
| } | ||||
| ``` | ||||
|  | ||||
| @ -35,7 +35,7 @@ The transform function returns a transform object. All properties of the object | ||||
|    - `rotation.origin` (either "local" i.e. rotate around its own center, "global" i.e. rotate around the scene's center, or a 3D point, defaults to "local") | ||||
|  | ||||
| ```js | ||||
| patternTransform(total_instances: u32, transform_function: FunctionParam, solid_set: SolidSet) -> [Solid] | ||||
| patternTransform(total_instances: integer, transform_function: FunctionParam, solid_set: SolidSet) -> [Solid] | ||||
| ``` | ||||
|  | ||||
|  | ||||
| @ -43,7 +43,7 @@ patternTransform(total_instances: u32, transform_function: FunctionParam, solid_ | ||||
|  | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `total_instances` | `u32` |  | Yes | | ||||
| | `total_instances` | `integer` |  | Yes | | ||||
| | `transform_function` | `FunctionParam` |  | Yes | | ||||
| | `solid_set` | [`SolidSet`](/docs/kcl/types/SolidSet) | A solid or a group of solids. | Yes | | ||||
|  | ||||
| @ -95,7 +95,8 @@ fn cube(length, center) { | ||||
|   p2 = [l + x, l + y] | ||||
|   p3 = [l + x, -l + y] | ||||
|  | ||||
|   return startSketchAt(p0) | ||||
|   return startSketchOn('XY') | ||||
|     |> startProfileAt(p0, %) | ||||
|     |> lineTo(p1, %) | ||||
|     |> lineTo(p2, %) | ||||
|     |> lineTo(p3, %) | ||||
| @ -132,7 +133,8 @@ fn cube(length, center) { | ||||
|   p2 = [l + x, l + y] | ||||
|   p3 = [l + x, -l + y] | ||||
|  | ||||
|   return startSketchAt(p0) | ||||
|   return startSketchOn('XY') | ||||
|     |> startProfileAt(p0, %) | ||||
|     |> lineTo(p1, %) | ||||
|     |> lineTo(p2, %) | ||||
|     |> lineTo(p3, %) | ||||
| @ -195,7 +197,8 @@ fn transform(i) { | ||||
|     { rotation = { angle = 45 * i } } | ||||
|   ] | ||||
| } | ||||
| startSketchAt([0, 0]) | ||||
| startSketchOn('XY') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> polygon({ | ||||
|        radius = 10, | ||||
|        numSides = 4, | ||||
|  | ||||
| @ -9,7 +9,7 @@ Just like patternTransform, but works on 2D sketches not 3D solids. | ||||
|  | ||||
|  | ||||
| ```js | ||||
| patternTransform2d(total_instances: u32, transform_function: FunctionParam, solid_set: SketchSet) -> [Sketch] | ||||
| patternTransform2d(total_instances: integer, transform_function: FunctionParam, solid_set: SketchSet) -> [Sketch] | ||||
| ``` | ||||
|  | ||||
|  | ||||
| @ -17,7 +17,7 @@ patternTransform2d(total_instances: u32, transform_function: FunctionParam, soli | ||||
|  | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `total_instances` | `u32` |  | Yes | | ||||
| | `total_instances` | `integer` |  | Yes | | ||||
| | `transform_function` | `FunctionParam` |  | Yes | | ||||
| | `solid_set` | [`SketchSet`](/docs/kcl/types/SketchSet) | A sketch or a group of sketches. | Yes | | ||||
|  | ||||
|  | ||||
| @ -43,7 +43,7 @@ fn sum(arr) { | ||||
|  | ||||
| /* The above is basically like this pseudo-code: | ||||
| fn sum(arr): | ||||
|     let sumSoFar = 0 | ||||
|     sumSoFar = 0 | ||||
|     for i in arr: | ||||
|         sumSoFar = add(sumSoFar, i) | ||||
|     return sumSoFar */ | ||||
| @ -61,7 +61,7 @@ assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6") | ||||
| // an anonymous `add` function as its parameter, instead of declaring a | ||||
| // named function outside. | ||||
| arr = [1, 2, 3] | ||||
| sum = reduce(arr, 0, (i, result_so_far) { | ||||
| sum = reduce(arr, 0, fn(i, result_so_far) { | ||||
|   return i + result_so_far | ||||
| }) | ||||
|  | ||||
| @ -79,12 +79,13 @@ fn decagon(radius) { | ||||
|   stepAngle = 1 / 10 * tau() | ||||
|  | ||||
|   // Start the decagon sketch at this point. | ||||
|   startOfDecagonSketch = startSketchAt([cos(0) * radius, sin(0) * radius]) | ||||
|   startOfDecagonSketch = startSketchOn('XY') | ||||
|     |> startProfileAt([cos(0) * radius, sin(0) * radius], %) | ||||
|  | ||||
|   // Use a `reduce` to draw the remaining decagon sides. | ||||
|   // For each number in the array 1..10, run the given function, | ||||
|     // Use a `reduce` to draw the remaining decagon sides. | ||||
|     // For each number in the array 1..10, run the given function, | ||||
|   // which takes a partially-sketched decagon and adds one more edge to it. | ||||
|   fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) { | ||||
|   fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) { | ||||
|     // Draw one edge of the decagon. | ||||
|     x = cos(stepAngle * i) * radius | ||||
|     y = sin(stepAngle * i) * radius | ||||
| @ -96,14 +97,15 @@ fn decagon(radius) { | ||||
|  | ||||
| /* The `decagon` above is basically like this pseudo-code: | ||||
| fn decagon(radius): | ||||
|     let stepAngle = (1/10) * tau() | ||||
|     let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)]) | ||||
|     stepAngle = (1/10) * tau() | ||||
|     plane = startSketchOn('XY') | ||||
|     startOfDecagonSketch = startProfileAt([(cos(0)*radius), (sin(0) * radius)], plane) | ||||
|  | ||||
|     // Here's the reduce part. | ||||
|     let partialDecagon = startOfDecagonSketch | ||||
|     partialDecagon = startOfDecagonSketch | ||||
|     for i in [1..10]: | ||||
|         let x = cos(stepAngle * i) * radius | ||||
|         let y = sin(stepAngle * i) * radius | ||||
|         x = cos(stepAngle * i) * radius | ||||
|         y = sin(stepAngle * i) * radius | ||||
|         partialDecagon = lineTo([x, y], partialDecagon) | ||||
|     fullDecagon = partialDecagon // it's now full | ||||
|     return fullDecagon */ | ||||
|  | ||||
| @ -28,7 +28,8 @@ segEnd(tag: TagIdentifier) -> [number] | ||||
|  | ||||
| ```js | ||||
| w = 15 | ||||
| cube = startSketchAt([0, 0]) | ||||
| cube = startSketchOn('XY') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([w, 0], %, $line1) | ||||
|   |> line([0, w], %, $line2) | ||||
|   |> line([-w, 0], %, $line3) | ||||
| @ -37,7 +38,8 @@ cube = startSketchAt([0, 0]) | ||||
|   |> extrude(5, %) | ||||
|  | ||||
| fn cylinder(radius, tag) { | ||||
|   return startSketchAt([0, 0]) | ||||
|   return startSketchOn('XY') | ||||
|     |> startProfileAt([0, 0], %) | ||||
|     |> circle({ | ||||
|          radius = radius, | ||||
|          center = segEnd(tag) | ||||
|  | ||||
| @ -28,7 +28,8 @@ segStart(tag: TagIdentifier) -> [number] | ||||
|  | ||||
| ```js | ||||
| w = 15 | ||||
| cube = startSketchAt([0, 0]) | ||||
| cube = startSketchOn('XY') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([w, 0], %, $line1) | ||||
|   |> line([0, w], %, $line2) | ||||
|   |> line([-w, 0], %, $line3) | ||||
| @ -37,7 +38,8 @@ cube = startSketchAt([0, 0]) | ||||
|   |> extrude(5, %) | ||||
|  | ||||
| fn cylinder(radius, tag) { | ||||
|   return startSketchAt([0, 0]) | ||||
|   return startSketchOn('XY') | ||||
|     |> startProfileAt([0, 0], %) | ||||
|     |> circle({ | ||||
|          radius = radius, | ||||
|          center = segStart(tag) | ||||
|  | ||||
| @ -4,6 +4,8 @@ excerpt: "Start a new 2-dimensional sketch at a given point on the 'XY' plane." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| **WARNING:** This function is deprecated. | ||||
|  | ||||
| Start a new 2-dimensional sketch at a given point on the 'XY' plane. | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										85410
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										55
									
								
								docs/kcl/sweep.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -54,7 +54,7 @@ We also have support for defining your own functions. Functions can take in any | ||||
| type of argument. Below is an example of the syntax: | ||||
|  | ||||
| ``` | ||||
| fn myFn = (x) => { | ||||
| fn myFn(x) { | ||||
|   return x | ||||
| } | ||||
| ``` | ||||
| @ -118,7 +118,7 @@ use the tag `rectangleSegmentA001` in any function or expression in the file. | ||||
| However if the code was written like this: | ||||
|  | ||||
| ``` | ||||
| fn rect = (origin) => { | ||||
| fn rect(origin) { | ||||
|   return startSketchOn('XZ') | ||||
|     |> startProfileAt(origin, %) | ||||
|     |> angledLine([0, 191.26], %, $rectangleSegmentA001) | ||||
| @ -146,7 +146,7 @@ Tags are accessible through the sketch group they are declared in. | ||||
| For example the following code works. | ||||
|  | ||||
| ``` | ||||
| fn rect = (origin) => { | ||||
| fn rect(origin) { | ||||
|   return startSketchOn('XZ') | ||||
|     |> startProfileAt(origin, %) | ||||
|     |> angledLine([0, 191.26], %, $rectangleSegmentA001) | ||||
|  | ||||
| @ -13,13 +13,18 @@ Data to draw an angled line. | ||||
|  | ||||
| An angle and length with explicitly named parameters | ||||
|  | ||||
| [`PolarCoordsData`](/docs/kcl/types/PolarCoordsData) | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `angle` |`number`| The angle of the line (in degrees). | No | | ||||
| | `length` |`number`| The length of the line. | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
							
								
								
									
										23
									
								
								docs/kcl/types/AppearanceData.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,23 @@ | ||||
| --- | ||||
| title: "AppearanceData" | ||||
| excerpt: "Data for appearance." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Data for appearance. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `color` |`string`| Color of the new material, a hex string like "#ff0000". | No | | ||||
| | `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No | | ||||
| | `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No | | ||||
|  | ||||
|  | ||||
| @ -1,161 +0,0 @@ | ||||
| --- | ||||
| title: "BinaryOperator" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts exactly one of the following:** | ||||
|  | ||||
| Add two numbers. | ||||
|  | ||||
| **enum:** `+` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Subtract two numbers. | ||||
|  | ||||
| **enum:** `-` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Multiply two numbers. | ||||
|  | ||||
| **enum:** `*` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Divide two numbers. | ||||
|  | ||||
| **enum:** `/` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Modulo two numbers. | ||||
|  | ||||
| **enum:** `%` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Raise a number to a power. | ||||
|  | ||||
| **enum:** `^` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Are two numbers equal? | ||||
|  | ||||
| **enum:** `==` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Are two numbers not equal? | ||||
|  | ||||
| **enum:** `!=` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Is left greater than right | ||||
|  | ||||
| **enum:** `>` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Is left greater than or equal to right | ||||
|  | ||||
| **enum:** `>=` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Is left less than right | ||||
|  | ||||
| **enum:** `<` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Is left less than or equal to right | ||||
|  | ||||
| **enum:** `<=` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,160 +0,0 @@ | ||||
| --- | ||||
| title: "BinaryPart" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts exactly one of the following:** | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `Literal`|  | No | | ||||
| | `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)|  | No | | ||||
| | `raw` |`string`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)|  | No | | ||||
| | `name` |`string`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `BinaryExpression`|  | No | | ||||
| | `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)|  | No | | ||||
| | `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)|  | No | | ||||
| | `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `CallExpression`|  | No | | ||||
| | `callee` |[`Identifier`](/docs/kcl/types/Identifier)|  | No | | ||||
| | `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `UnaryExpression`|  | No | | ||||
| | `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)|  | No | | ||||
| | `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `MemberExpression`|  | No | | ||||
| | `object` |[`MemberObject`](/docs/kcl/types/MemberObject)|  | No | | ||||
| | `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)|  | No | | ||||
| | `computed` |`boolean`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `IfExpression`|  | No | | ||||
| | `cond` |[`Expr`](/docs/kcl/types/Expr)|  | No | | ||||
| | `then_val` |[`Program`](/docs/kcl/types/Program)|  | No | | ||||
| | `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`|  | No | | ||||
| | `final_else` |[`Program`](/docs/kcl/types/Program)|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,97 +0,0 @@ | ||||
| --- | ||||
| title: "BodyItem" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts exactly one of the following:** | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `ImportStatement`|  | No | | ||||
| | `items` |`[` [`ImportItem`](/docs/kcl/types/ImportItem) `]`|  | No | | ||||
| | `path` |`string`|  | No | | ||||
| | `raw_path` |`string`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `ExpressionStatement`|  | No | | ||||
| | `expression` |[`Expr`](/docs/kcl/types/Expr)|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `VariableDeclaration`|  | No | | ||||
| | `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`|  | No | | ||||
| | `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)|  | No | | ||||
| | `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `ReturnStatement`|  | No | | ||||
| | `argument` |[`Expr`](/docs/kcl/types/Expr)|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,41 +0,0 @@ | ||||
| --- | ||||
| title: "CommentStyle" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts exactly one of the following:** | ||||
|  | ||||
| Like // foo | ||||
|  | ||||
| **enum:** `line` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Like /* foo */ | ||||
|  | ||||
| **enum:** `block` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,24 +0,0 @@ | ||||
| --- | ||||
| title: "ElseIf" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `cond` |[`Expr`](/docs/kcl/types/Expr)|  | No | | ||||
| | `then_val` |[`Program`](/docs/kcl/types/Program)|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| @ -1,16 +0,0 @@ | ||||
| --- | ||||
| title: "EnvironmentRef" | ||||
| excerpt: "An index pointing to an environment." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| An index pointing to an environment. | ||||
|  | ||||
| **Type:** `integer` (`uint`) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,317 +0,0 @@ | ||||
| --- | ||||
| title: "Expr" | ||||
| excerpt: "An expression can be evaluated to yield a single KCL value." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| An expression can be evaluated to yield a single KCL value. | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts exactly one of the following:** | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `Literal`|  | No | | ||||
| | `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `raw` |`string`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)|  | No | | ||||
| | `name` |`string`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: [`TagDeclarator`](/docs/kcl/types#tag-declaration)|  | No | | ||||
| | `value` |`string`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `BinaryExpression`|  | No | | ||||
| | `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: [`FunctionExpression`](/docs/kcl/types/FunctionExpression)|  | No | | ||||
| | `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`|  | No | | ||||
| | `body` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `CallExpression`|  | No | | ||||
| | `callee` |[`Identifier`](/docs/kcl/types/Identifier)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `PipeExpression`|  | No | | ||||
| | `body` |`[` [`Expr`](/docs/kcl/types/Expr) `]`|  | No | | ||||
| | `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `PipeSubstitution`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `ArrayExpression`|  | No | | ||||
| | `elements` |`[` [`Expr`](/docs/kcl/types/Expr) `]`|  | No | | ||||
| | `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `ArrayRangeExpression`|  | No | | ||||
| | `startElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `endElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `endInclusive` |`boolean`| Is the `end_element` included in the range? | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `ObjectExpression`|  | No | | ||||
| | `properties` |`[` [`ObjectProperty`](/docs/kcl/types/ObjectProperty) `]`|  | No | | ||||
| | `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `MemberExpression`|  | No | | ||||
| | `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `computed` |`boolean`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `UnaryExpression`|  | No | | ||||
| | `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `IfExpression`|  | No | | ||||
| | `cond` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `then_val` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`|  | No | | ||||
| | `final_else` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
| KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application). | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `None`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,24 +0,0 @@ | ||||
| --- | ||||
| title: "FunctionExpression" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`|  | No | | ||||
| | `body` |[`Program`](/docs/kcl/types/Program)|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| @ -1,23 +0,0 @@ | ||||
| --- | ||||
| title: "Identifier" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `name` |`string`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| @ -1,24 +0,0 @@ | ||||
| --- | ||||
| title: "ImportItem" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `name` |[`Identifier`](/docs/kcl/types/Identifier)| Name of the item to import. | No | | ||||
| | `alias` |[`Identifier`](/docs/kcl/types/Identifier)| Rename the item using an identifier after "as". | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| @ -1,16 +0,0 @@ | ||||
| --- | ||||
| title: "ItemVisibility" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
| **enum:** `default`, `export` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -12,5 +12,10 @@ KCL value for an optional parameter which was not given an argument. (remember, | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -317,7 +317,6 @@ Data for an imported geometry. | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `Function`|  | No | | ||||
| | `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| Any KCL value. | No | | ||||
| | `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
| @ -330,6 +329,23 @@ Data for an imported geometry. | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `Module`|  | No | | ||||
| | `value` |[`ModuleId`](/docs/kcl/types/ModuleId)| Any KCL value. | No | | ||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
|  | ||||
| @ -1,56 +0,0 @@ | ||||
| --- | ||||
| title: "LiteralIdentifier" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts exactly one of the following:** | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)|  | No | | ||||
| | `name` |`string`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `Literal`|  | No | | ||||
| | `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)|  | No | | ||||
| | `raw` |`string`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,47 +0,0 @@ | ||||
| --- | ||||
| title: "LiteralValue" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts any of the following:** | ||||
|  | ||||
|  | ||||
| **Type:** `number` (`double`) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `string` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `boolean` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,57 +0,0 @@ | ||||
| --- | ||||
| title: "MemberObject" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts exactly one of the following:** | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `MemberExpression`|  | No | | ||||
| | `object` |[`MemberObject`](/docs/kcl/types/MemberObject)|  | No | | ||||
| | `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)|  | No | | ||||
| | `computed` |`boolean`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)|  | No | | ||||
| | `name` |`string`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										16
									
								
								docs/kcl/types/ModuleId.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,16 @@ | ||||
| --- | ||||
| title: "ModuleId" | ||||
| excerpt: "Identifier of a source file.  Uses a u32 to keep the size small." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Identifier of a source file.  Uses a u32 to keep the size small. | ||||
|  | ||||
| **Type:** `integer` (`uint32`) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,22 +0,0 @@ | ||||
| --- | ||||
| title: "NonCodeMeta" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `nonCodeNodes` |`object`|  | No | | ||||
| | `startNodes` |`[` [`NonCodeNode`](/docs/kcl/types/NonCodeNode) `]`|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
|  | ||||
|  | ||||
| @ -1,23 +0,0 @@ | ||||
| --- | ||||
| title: "NonCodeNode" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `value` |[`NonCodeValue`](/docs/kcl/types/NonCodeValue)|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| @ -1,86 +0,0 @@ | ||||
| --- | ||||
| title: "NonCodeValue" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts exactly one of the following:** | ||||
|  | ||||
| An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `inlineComment`|  | No | | ||||
| | `value` |`string`|  | No | | ||||
| | `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
| A block comment. An example of this is the following: ```python,no_run /* This is a block comment */ 1 + 1 ``` Now this is important. The block comment is attached to the next line. This is always the case. Also the block comment doesn't have a new line above it. If it did it would be a `NewLineBlockComment`. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `blockComment`|  | No | | ||||
| | `value` |`string`|  | No | | ||||
| | `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
| A block comment that has a new line above it. The user explicitly added a new line above the block comment. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `newLineBlockComment`|  | No | | ||||
| | `value` |`string`|  | No | | ||||
| | `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `type` |enum: `newLine`|  | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,24 +0,0 @@ | ||||
| --- | ||||
| title: "ObjectProperty" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `key` |[`Identifier`](/docs/kcl/types/Identifier)|  | No | | ||||
| | `value` |[`Expr`](/docs/kcl/types/Expr)|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| @ -1,23 +0,0 @@ | ||||
| --- | ||||
| title: "Parameter" | ||||
| excerpt: "Parameter of a KCL function." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Parameter of a KCL function. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `identifier` |[`Identifier`](/docs/kcl/types/Identifier)| The parameter's label or name. | No | | ||||
| | `optional` |`boolean`| Is the parameter optional? | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
|  | ||||
|  | ||||
| @ -1,26 +0,0 @@ | ||||
| --- | ||||
| title: "Program" | ||||
| excerpt: "A KCL program top level, or function body." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| A KCL program top level, or function body. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `body` |`[` [`BodyItem`](/docs/kcl/types/BodyItem) `]`|  | No | | ||||
| | `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| A KCL program top level, or function body. | No | | ||||
| | `shebang` |[`Shebang`](/docs/kcl/types/Shebang)|  | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| @ -1,23 +0,0 @@ | ||||
| --- | ||||
| title: "Shebang" | ||||
| excerpt: "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ``` | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `content` |`string`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
							
								
								
									
										23
									
								
								docs/kcl/types/SweepData.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,23 @@ | ||||
| --- | ||||
| title: "SweepData" | ||||
| excerpt: "Data for a sweep." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Data for a sweep. | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No | | ||||
| | `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No | | ||||
| | `tolerance` |`number`| Tolerance for the sweep operation. | No | | ||||
|  | ||||
|  | ||||
| @ -1,15 +0,0 @@ | ||||
| --- | ||||
| title: "Uint" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
| **Type:** `integer` (`uint32`) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,41 +0,0 @@ | ||||
| --- | ||||
| title: "UnaryOperator" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts exactly one of the following:** | ||||
|  | ||||
| Negate a number. | ||||
|  | ||||
| **enum:** `-` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Negate a boolean. | ||||
|  | ||||
| **enum:** `!` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,24 +0,0 @@ | ||||
| --- | ||||
| title: "VariableDeclarator" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
| **Type:** `object` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Properties | ||||
|  | ||||
| | Property | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `id` |[`Identifier`](/docs/kcl/types/Identifier)| The identifier of the variable. | No | | ||||
| | `init` |[`Expr`](/docs/kcl/types/Expr)| The value of the variable. | No | | ||||
| | `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`|  | No | | ||||
| | `start` |`integer`|  | No | | ||||
| | `end` |`integer`|  | No | | ||||
|  | ||||
|  | ||||
| @ -1,41 +0,0 @@ | ||||
| --- | ||||
| title: "VariableKind" | ||||
| excerpt: "" | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| **This schema accepts exactly one of the following:** | ||||
|  | ||||
| Declare a named constant. | ||||
|  | ||||
| **enum:** `const` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
| Declare a function. | ||||
|  | ||||
| **enum:** `fn` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,22 +1,11 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
|  | ||||
| import { setupElectron, tearDown } from './test-utils' | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
| import { test, expect } from './zoo-test' | ||||
|  | ||||
| test.describe('Electron app header tests', () => { | ||||
|   test( | ||||
|     'Open Command Palette button has correct shortcut', | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browserName }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async () => {}, | ||||
|       }) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     async ({ page }, testInfo) => { | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|       // No space before the shortcut since it checks textContent. | ||||
|       let text | ||||
| @ -34,21 +23,14 @@ test.describe('Electron app header tests', () => { | ||||
|       const commandsButton = page.getByRole('button', { name: 'Commands' }) | ||||
|       await expect(commandsButton).toBeVisible() | ||||
|       await expect(commandsButton).toHaveText(text) | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     'User settings has correct shortcut', | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browserName }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async () => {}, | ||||
|       }) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     async ({ page }, testInfo) => { | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|       // Open the user sidebar menu. | ||||
|       await page.getByTestId('user-sidebar-toggle').click() | ||||
| @ -59,8 +41,6 @@ test.describe('Electron app header tests', () => { | ||||
|       const userSettingsButton = page.getByTestId('user-settings') | ||||
|       await expect(userSettingsButton).toBeVisible() | ||||
|       await expect(userSettingsButton).toHaveText(text) | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
| }) | ||||
|  | ||||
| @ -1,29 +1,26 @@ | ||||
| import { test, expect, Page } from '@playwright/test' | ||||
| import { test, expect, Page } from './zoo-test' | ||||
| import { | ||||
|   getUtils, | ||||
|   TEST_COLORS, | ||||
|   setup, | ||||
|   tearDown, | ||||
|   commonPoints, | ||||
|   PERSIST_MODELING_CONTEXT, | ||||
| } from './test-utils' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }, testInfo) => { | ||||
|   await setup(context, page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
| import { HomePageFixture } from './fixtures/homePageFixture' | ||||
|  | ||||
| test.setTimeout(120000) | ||||
|  | ||||
| async function doBasicSketch(page: Page, openPanes: string[]) { | ||||
| async function doBasicSketch( | ||||
|   page: Page, | ||||
|   homePage: HomePageFixture, | ||||
|   openPanes: string[] | ||||
| ) { | ||||
|   const u = await getUtils(page) | ||||
|   await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|   await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|   const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|  | ||||
|   await u.waitForAuthSkipAppStart() | ||||
|   await homePage.goToModelingScene() | ||||
|   await u.waitForPageLoad() | ||||
|   await page.waitForTimeout(1000) | ||||
|   await u.openDebugPanel() | ||||
|  | ||||
|   // If we have the code pane open, we should see the code. | ||||
| @ -148,13 +145,11 @@ async function doBasicSketch(page: Page, openPanes: string[]) { | ||||
| } | ||||
|  | ||||
| test.describe('Basic sketch', () => { | ||||
|   test('code pane open at start', { tag: ['@skipWin'] }, async ({ page }) => { | ||||
|     // Skip on windows it is being weird. | ||||
|     test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|     await doBasicSketch(page, ['code']) | ||||
|   test.fixme('code pane open at start', async ({ page, homePage }) => { | ||||
|     await doBasicSketch(page, homePage, ['code']) | ||||
|   }) | ||||
|  | ||||
|   test('code pane closed at start', async ({ page }) => { | ||||
|   test('code pane closed at start', async ({ page, homePage }) => { | ||||
|     // Load the app with the code panes | ||||
|     await page.addInitScript(async (persistModelingContext) => { | ||||
|       localStorage.setItem( | ||||
| @ -162,6 +157,6 @@ test.describe('Basic sketch', () => { | ||||
|         JSON.stringify({ openPanes: [] }) | ||||
|       ) | ||||
|     }, PERSIST_MODELING_CONTEXT) | ||||
|     await doBasicSketch(page, []) | ||||
|     await doBasicSketch(page, homePage, []) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -1,27 +1,21 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { getUtils, setup, tearDown } from './test-utils' | ||||
| import { test, expect, Page } from './zoo-test' | ||||
| import { HomePageFixture } from './fixtures/homePageFixture' | ||||
| import { getUtils } from './test-utils' | ||||
| import { EngineCommand } from 'lang/std/artifactGraph' | ||||
| import { uuidv4 } from 'lib/utils' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }, testInfo) => { | ||||
|   await setup(context, page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.describe('Can create sketches on all planes and their back sides', () => { | ||||
|   const sketchOnPlaneAndBackSideTest = async ( | ||||
|     page: any, | ||||
|     page: Page, | ||||
|     homePage: HomePageFixture, | ||||
|     plane: string, | ||||
|     clickCoords: { x: number; y: number } | ||||
|   ) => { | ||||
|     const u = await getUtils(page) | ||||
|     const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.openDebugPanel() | ||||
|  | ||||
|     const coord = | ||||
| @ -83,32 +77,39 @@ test.describe('Can create sketches on all planes and their back sides', () => { | ||||
|     await u.clearCommandLogs() | ||||
|     await u.removeCurrentCode() | ||||
|   } | ||||
|   test('XY', async ({ page }) => { | ||||
|   test('XY', async ({ page, homePage }) => { | ||||
|     await sketchOnPlaneAndBackSideTest( | ||||
|       page, | ||||
|       homePage, | ||||
|       'XY', | ||||
|       { x: 600, y: 388 } // red plane | ||||
|       // { x: 600, y: 400 }, // red plane // clicks grid helper and that causes problems, should fix so that these coords work too. | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   test('YZ', async ({ page }) => { | ||||
|     await sketchOnPlaneAndBackSideTest(page, 'YZ', { x: 700, y: 250 }) // green plane | ||||
|   test('YZ', async ({ page, homePage }) => { | ||||
|     await sketchOnPlaneAndBackSideTest(page, homePage, 'YZ', { x: 700, y: 250 }) // green plane | ||||
|   }) | ||||
|  | ||||
|   test('XZ', async ({ page }) => { | ||||
|     await sketchOnPlaneAndBackSideTest(page, '-XZ', { x: 700, y: 80 }) // blue plane | ||||
|   test('XZ', async ({ page, homePage }) => { | ||||
|     await sketchOnPlaneAndBackSideTest(page, homePage, '-XZ', { x: 700, y: 80 }) // blue plane | ||||
|   }) | ||||
|  | ||||
|   test('-XY', async ({ page }) => { | ||||
|     await sketchOnPlaneAndBackSideTest(page, '-XY', { x: 600, y: 118 }) // back of red plane | ||||
|   test('-XY', async ({ page, homePage }) => { | ||||
|     await sketchOnPlaneAndBackSideTest(page, homePage, '-XY', { | ||||
|       x: 600, | ||||
|       y: 118, | ||||
|     }) // back of red plane | ||||
|   }) | ||||
|  | ||||
|   test('-YZ', async ({ page }) => { | ||||
|     await sketchOnPlaneAndBackSideTest(page, '-YZ', { x: 700, y: 219 }) // back of green plane | ||||
|   test('-YZ', async ({ page, homePage }) => { | ||||
|     await sketchOnPlaneAndBackSideTest(page, homePage, '-YZ', { | ||||
|       x: 700, | ||||
|       y: 219, | ||||
|     }) // back of green plan | ||||
|   }) | ||||
|  | ||||
|   test('-XZ', async ({ page }) => { | ||||
|     await sketchOnPlaneAndBackSideTest(page, 'XZ', { x: 700, y: 427 }) // back of blue plane | ||||
|   test('-XZ', async ({ page, homePage }) => { | ||||
|     await sketchOnPlaneAndBackSideTest(page, homePage, 'XZ', { x: 700, y: 427 }) // back of blue plane | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -1,28 +1,15 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { test, expect } from './zoo-test' | ||||
|  | ||||
| import { | ||||
|   getUtils, | ||||
|   setup, | ||||
|   setupElectron, | ||||
|   tearDown, | ||||
|   executorInputPath, | ||||
| } from './test-utils' | ||||
| import { getUtils, executorInputPath } from './test-utils' | ||||
| import { join } from 'path' | ||||
| import { bracket } from 'lib/exampleKcl' | ||||
| import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates' | ||||
| import fsp from 'fs/promises' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }, testInfo) => { | ||||
|   await setup(context, page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.describe('Code pane and errors', () => { | ||||
|   test('Typing KCL errors induces a badge on the code pane button', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
| @ -31,18 +18,18 @@ test.describe('Code pane and errors', () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `// Extruded Triangle | ||||
| sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([10, 0], %) | ||||
|   |> line([-5, 10], %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(5, sketch001)` | ||||
|   sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt([0, 0], %) | ||||
|     |> line([10, 0], %) | ||||
|     |> line([-5, 10], %) | ||||
|     |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|     |> close(%) | ||||
|   extrude001 = extrude(5, sketch001)` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // wait for execution done | ||||
|     await u.openDebugPanel() | ||||
| @ -62,11 +49,11 @@ extrude001 = extrude(5, sketch001)` | ||||
|     await expect(codePaneButtonHolder).toContainText('notification') | ||||
|   }) | ||||
|  | ||||
|   test('Opening and closing the code pane will consistently show error diagnostics', async ({ | ||||
|   test.skip('Opening and closing the code pane will consistently show error diagnostics', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|     editor, | ||||
|   }) => { | ||||
|     await page.goto('http://localhost:3000') | ||||
|  | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|     // Load the app with the working starter code | ||||
| @ -74,8 +61,8 @@ extrude001 = extrude(5, sketch001)` | ||||
|       localStorage.setItem('persistCode', code) | ||||
|     }, bracket) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 900 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await page.setBodyDimensions({ width: 1200, height: 900 }) | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // wait for execution done | ||||
|     await u.openDebugPanel() | ||||
| @ -91,8 +78,9 @@ extrude001 = extrude(5, sketch001)` | ||||
|     await expect(codePaneButtonHolder).not.toContainText('notification') | ||||
|  | ||||
|     // Delete a character to break the KCL | ||||
|     await u.openKclCodePanel() | ||||
|     await page.getByText('thickness, bracketLeg1Sketch)').click() | ||||
|     await editor.openPane() | ||||
|     await editor.scrollToText('thickness, bracketLeg1Sketch)') | ||||
|     await page.getByText('extrude(thickness, bracketLeg1Sketch)').click() | ||||
|     await page.keyboard.press('Backspace') | ||||
|  | ||||
|     // Ensure that a badge appears on the button | ||||
| @ -116,7 +104,10 @@ extrude001 = extrude(5, sketch001)` | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
|  | ||||
|     // Open the code pane | ||||
|     await u.openKclCodePanel() | ||||
|     await editor.openPane() | ||||
|  | ||||
|     // Go to our problematic code again (missing closing paren!) | ||||
|     await editor.scrollToText('extrude(thickness, bracketLeg1Sketch') | ||||
|  | ||||
|     // Ensure that a badge appears on the button | ||||
|     await expect(codePaneButtonHolder).toContainText('notification') | ||||
| @ -129,59 +120,58 @@ extrude001 = extrude(5, sketch001)` | ||||
|     await expect(page.locator('.cm-tooltip').first()).toBeVisible() | ||||
|   }) | ||||
|  | ||||
|   test('When error is not in view you can click the badge to scroll to it', async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|   test.fixme( | ||||
|     'When error is not in view you can click the badge to scroll to it', | ||||
|     async ({ page, homePage, context }) => { | ||||
|       // Load the app with the working starter code | ||||
|       await context.addInitScript((code) => { | ||||
|         localStorage.setItem('persistCode', code) | ||||
|       }, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW) | ||||
|  | ||||
|     // Load the app with the working starter code | ||||
|     await page.addInitScript((code) => { | ||||
|       localStorage.setItem('persistCode', code) | ||||
|     }, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW) | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|       await page.waitForTimeout(1000) | ||||
|  | ||||
|     await page.waitForTimeout(1000) | ||||
|       // Ensure badge is present | ||||
|       const codePaneButtonHolder = page.locator('#code-button-holder') | ||||
|       await expect(codePaneButtonHolder).toContainText('notification') | ||||
|  | ||||
|     // Ensure badge is present | ||||
|     const codePaneButtonHolder = page.locator('#code-button-holder') | ||||
|     await expect(codePaneButtonHolder).toContainText('notification') | ||||
|       // Ensure we have no errors in the gutter, since error out of view. | ||||
|       await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
|  | ||||
|     // Ensure we have no errors in the gutter, since error out of view. | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
|       // Click the badge. | ||||
|       const badge = page.locator('#code-badge') | ||||
|       await expect(badge).toBeVisible() | ||||
|       await badge.click() | ||||
|  | ||||
|     // Click the badge. | ||||
|     const badge = page.locator('#code-badge') | ||||
|     await expect(badge).toBeVisible() | ||||
|     await badge.click() | ||||
|       // Ensure we have an error diagnostic. | ||||
|       await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() | ||||
|  | ||||
|     // Ensure we have an error diagnostic. | ||||
|     await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() | ||||
|  | ||||
|     // Hover over the error to see the error message | ||||
|     await page.hover('.cm-lint-marker-error') | ||||
|     await expect( | ||||
|       page | ||||
|         .getByText( | ||||
|           'sketch profile must lie entirely on one side of the revolution axis' | ||||
|         ) | ||||
|         .first() | ||||
|     ).toBeVisible() | ||||
|   }) | ||||
|       // Hover over the error to see the error message | ||||
|       await page.hover('.cm-lint-marker-error') | ||||
|       await expect( | ||||
|         page | ||||
|           .getByText( | ||||
|             'Modeling command failed: [ApiError { error_code: InternalEngine, message: "Solid3D revolve failed:  sketch profile must lie entirely on one side of the revolution axis" }]' | ||||
|           ) | ||||
|           .first() | ||||
|       ).toBeVisible() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test('When error is not in view WITH LINTS you can click the badge to scroll to it', async ({ | ||||
|     context, | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|     // Load the app with the working starter code | ||||
|     await page.addInitScript((code) => { | ||||
|     await context.addInitScript((code) => { | ||||
|       localStorage.setItem('persistCode', code) | ||||
|     }, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await page.waitForTimeout(1000) | ||||
|  | ||||
| @ -241,32 +231,29 @@ extrude001 = extrude(5, sketch001)` | ||||
| test( | ||||
|   'Opening multiple panes persists when switching projects', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|   async ({ context, page }, testInfo) => { | ||||
|     // Setup multiple projects. | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         const routerTemplateDir = join(dir, 'router-template-slate') | ||||
|         const bracketDir = join(dir, 'bracket') | ||||
|         await Promise.all([ | ||||
|           fsp.mkdir(routerTemplateDir, { recursive: true }), | ||||
|           fsp.mkdir(bracketDir, { recursive: true }), | ||||
|         ]) | ||||
|         await Promise.all([ | ||||
|           fsp.copyFile( | ||||
|             executorInputPath('router-template-slate.kcl'), | ||||
|             join(routerTemplateDir, 'main.kcl') | ||||
|           ), | ||||
|           fsp.copyFile( | ||||
|             executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|             join(bracketDir, 'main.kcl') | ||||
|           ), | ||||
|         ]) | ||||
|       }, | ||||
|     await context.folderSetupFn(async (dir) => { | ||||
|       const routerTemplateDir = join(dir, 'router-template-slate') | ||||
|       const bracketDir = join(dir, 'bracket') | ||||
|       await Promise.all([ | ||||
|         fsp.mkdir(routerTemplateDir, { recursive: true }), | ||||
|         fsp.mkdir(bracketDir, { recursive: true }), | ||||
|       ]) | ||||
|       await Promise.all([ | ||||
|         fsp.copyFile( | ||||
|           executorInputPath('router-template-slate.kcl'), | ||||
|           join(routerTemplateDir, 'main.kcl') | ||||
|         ), | ||||
|         fsp.copyFile( | ||||
|           executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|           join(bracketDir, 'main.kcl') | ||||
|         ), | ||||
|       ]) | ||||
|     }) | ||||
|  | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await test.step('Opening the bracket project should load', async () => { | ||||
|       await expect(page.getByText('bracket')).toBeVisible() | ||||
| @ -309,30 +296,21 @@ test( | ||||
|       await expect(page.locator('#variables-pane')).toBeVisible() | ||||
|       await expect(page.locator('#logs-pane')).toBeVisible() | ||||
|     }) | ||||
|  | ||||
|     await electronApp.close() | ||||
|   } | ||||
| ) | ||||
|  | ||||
| test( | ||||
|   'external change of file contents are reflected in editor', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|   async ({ context, page }, testInfo) => { | ||||
|     const PROJECT_DIR_NAME = 'lee-was-here' | ||||
|     const { | ||||
|       electronApp, | ||||
|       page, | ||||
|       dir: projectsDir, | ||||
|     } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         const aProjectDir = join(dir, PROJECT_DIR_NAME) | ||||
|         await fsp.mkdir(aProjectDir, { recursive: true }) | ||||
|       }, | ||||
|     const { dir: projectsDir } = await context.folderSetupFn(async (dir) => { | ||||
|       const aProjectDir = join(dir, PROJECT_DIR_NAME) | ||||
|       await fsp.mkdir(aProjectDir, { recursive: true }) | ||||
|     }) | ||||
|  | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await test.step('Open the project', async () => { | ||||
|       await expect(page.getByText(PROJECT_DIR_NAME)).toBeVisible() | ||||
| @ -351,7 +329,5 @@ test( | ||||
|       ) | ||||
|       await u.editorTextMatches(content) | ||||
|     }) | ||||
|  | ||||
|     await electronApp.close() | ||||
|   } | ||||
| ) | ||||
|  | ||||
| @ -1,37 +1,30 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { test, expect } from './zoo-test' | ||||
|  | ||||
| import { getUtils, setup, tearDown } from './test-utils' | ||||
| import { getUtils } from './test-utils' | ||||
| import { KCL_DEFAULT_LENGTH } from 'lib/constants' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }, testInfo) => { | ||||
|   await setup(context, page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.describe('Command bar tests', () => { | ||||
|   test('Extrude from command bar selects extrude line after', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch001 = startSketchOn('XY') | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> xLine(-20, %) | ||||
|     |> close(%) | ||||
|       ` | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> xLine(-20, %) | ||||
|   |> close(%) | ||||
|     ` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
| @ -52,24 +45,25 @@ test.describe('Command bar tests', () => { | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   test('Fillet from command bar', async ({ page }) => { | ||||
|   // 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)` | ||||
|     |> 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.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.closeDebugPanel() | ||||
| @ -93,10 +87,10 @@ extrude001 = extrude(-10, sketch001)` | ||||
|  | ||||
|   test('Command bar can change a setting, and switch back and forth between arguments', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     const commandBarButton = page.getByRole('button', { name: 'Commands' }) | ||||
|     const cmdSearchBar = page.getByPlaceholder('Search commands') | ||||
| @ -153,7 +147,7 @@ extrude001 = extrude(-10, sketch001)` | ||||
|     // Check that the visibility changed | ||||
|     await expect(paneSelector).not.toBeVisible() | ||||
|  | ||||
|     commandOptionInput = page.getByPlaceholder('off') | ||||
|     commandOptionInput = page.locator('[id="option-input"]') | ||||
|  | ||||
|     // Test case for https://github.com/KittyCAD/modeling-app/issues/2882 | ||||
|     await commandBarButton.click() | ||||
| @ -174,10 +168,10 @@ extrude001 = extrude(-10, sketch001)` | ||||
|  | ||||
|   test('Command bar keybinding works from code editor and can change a setting', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Start Sketch' }) | ||||
| @ -221,25 +215,25 @@ extrude001 = extrude(-10, sketch001)` | ||||
|     await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) | ||||
|   }) | ||||
|  | ||||
|   test('Can extrude from the command bar', async ({ page }) => { | ||||
|   test('Can extrude from the command bar', async ({ page, homePage }) => { | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `distance = sqrt(20) | ||||
|       sketch001 = startSketchOn('XZ') | ||||
|       |> startProfileAt([-6.95, 10.98], %) | ||||
|       |> line([25.1, 0.41], %) | ||||
|       |> line([0.73, -20.93], %) | ||||
|       |> line([-23.44, 0.52], %) | ||||
|       |> close(%) | ||||
|           ` | ||||
|     sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt([-6.95, 10.98], %) | ||||
|     |> line([25.1, 0.41], %) | ||||
|     |> line([0.73, -20.93], %) | ||||
|     |> line([-23.44, 0.52], %) | ||||
|     |> close(%) | ||||
|         ` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // Make sure the stream is up | ||||
|     await u.openDebugPanel() | ||||
| @ -293,26 +287,19 @@ extrude001 = extrude(-10, sketch001)` | ||||
|     await continueButton.click() | ||||
|     await submitButton.click() | ||||
|  | ||||
|     // Check that the code was updated | ||||
|     await u.waitForCmdReceive('extrude') | ||||
|     // Unfortunately this indentation seems to matter for the test | ||||
|     await expect(page.locator('.cm-content')).toHaveText( | ||||
|       `distance = sqrt(20) | ||||
| distance001 = ${KCL_DEFAULT_LENGTH} | ||||
| sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt([-6.95, 10.98], %) | ||||
|     |> line([25.1, 0.41], %) | ||||
|     |> line([0.73, -20.93], %) | ||||
|     |> line([-23.44, 0.52], %) | ||||
|     |> close(%) | ||||
| extrude001 = extrude(distance001, sketch001)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines | ||||
|  | ||||
|     await expect(page.locator('.cm-content')).toContainText( | ||||
|       'extrude001 = extrude(distance001, sketch001)' | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   test('Can switch between sketch tools via command bar', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|   test('Can switch between sketch tools via command bar', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     const sketchButton = page.getByRole('button', { name: 'Start Sketch' }) | ||||
|     const cmdBarButton = page.getByRole('button', { name: 'Commands' }) | ||||
|  | ||||
| @ -1,23 +1,16 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { getUtils, setup, tearDown } from './test-utils' | ||||
| import { test, expect } from './zoo-test' | ||||
| import { getUtils } from './test-utils' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }, testInfo) => { | ||||
|   await setup(context, page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
| test.describe('Copilot ghost text', () => { | ||||
|   // eslint-disable-next-line jest/valid-title | ||||
|   test.skip(true, 'Needs to get covered again') | ||||
|  | ||||
|   test('completes code in empty file', async ({ page }) => { | ||||
|   test('completes code in empty file', async ({ page, homePage }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
| @ -52,12 +45,13 @@ test.describe('Copilot ghost text', () => { | ||||
|  | ||||
|   test.skip('copilot disabled in sketch mode no select plane', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
| @ -101,12 +95,13 @@ test.describe('Copilot ghost text', () => { | ||||
|  | ||||
|   test('copilot disabled in sketch mode after selecting plane', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
| @ -184,12 +179,12 @@ test.describe('Copilot ghost text', () => { | ||||
|     await expect(page.locator('.cm-ghostText')).not.toBeVisible() | ||||
|   }) | ||||
|  | ||||
|   test('ArrowUp in code rejects the suggestion', async ({ page }) => { | ||||
|   test('ArrowUp in code rejects the suggestion', async ({ page, homePage }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
| @ -212,12 +207,15 @@ test.describe('Copilot ghost text', () => { | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|   }) | ||||
|  | ||||
|   test('ArrowDown in code rejects the suggestion', async ({ page }) => { | ||||
|   test('ArrowDown in code rejects the suggestion', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
| @ -240,12 +238,15 @@ test.describe('Copilot ghost text', () => { | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|   }) | ||||
|  | ||||
|   test('ArrowLeft in code rejects the suggestion', async ({ page }) => { | ||||
|   test('ArrowLeft in code rejects the suggestion', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
| @ -268,12 +269,15 @@ test.describe('Copilot ghost text', () => { | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|   }) | ||||
|  | ||||
|   test('ArrowRight in code rejects the suggestion', async ({ page }) => { | ||||
|   test('ArrowRight in code rejects the suggestion', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
| @ -296,12 +300,12 @@ test.describe('Copilot ghost text', () => { | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|   }) | ||||
|  | ||||
|   test('Enter in code scoots it down', async ({ page }) => { | ||||
|   test('Enter in code scoots it down', async ({ page, homePage }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
| @ -326,12 +330,15 @@ test.describe('Copilot ghost text', () => { | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   test('Ctrl+shift+z in code rejects the suggestion', async ({ page }) => { | ||||
|   test('Ctrl+shift+z in code rejects the suggestion', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
| @ -360,12 +367,13 @@ test.describe('Copilot ghost text', () => { | ||||
|  | ||||
|   test('Ctrl+z in code rejects the suggestion and undos the last code', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await page.waitForTimeout(800) | ||||
|     await u.codeLocator.click() | ||||
| @ -420,98 +428,107 @@ test.describe('Copilot ghost text', () => { | ||||
|     await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() | ||||
|  | ||||
|     // TODO when we make codemirror a widget, we can test this. | ||||
|     //await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|   }) | ||||
|     //await expect(page.locator('.cm-content')).toHaveText(``) }) | ||||
|  | ||||
|   test('delete in code rejects the suggestion', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     test('delete in code rejects the suggestion', async ({ | ||||
|       page, | ||||
|       homePage, | ||||
|     }) => { | ||||
|       const u = await getUtils(page) | ||||
|       // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|       await u.codeLocator.click() | ||||
|       await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|  | ||||
|     await expect(page.locator('.cm-ghostText')).not.toBeVisible() | ||||
|     await page.waitForTimeout(500) | ||||
|     await page.keyboard.press('Enter') | ||||
|     await page.keyboard.press('Enter') | ||||
|     await page.keyboard.press('Enter') | ||||
|     await expect(page.locator('.cm-ghostText').first()).toBeVisible() | ||||
|     await expect(page.locator('.cm-content')).toHaveText( | ||||
|       `fn cube = (pos, scale) => {  sg = startSketchOn('XY')    |> startProfileAt(pos, %)    |> line([0, scale], %)    |> line([scale, 0], %)    |> line([0, -scale], %)  return sg}part001 = cube([0,0], 20)    |> close(%)    |> extrude(20, %)` | ||||
|     ) | ||||
|     await expect(page.locator('.cm-ghostText').first()).toHaveText( | ||||
|       `fn cube = (pos, scale) => {` | ||||
|     ) | ||||
|       await expect(page.locator('.cm-ghostText')).not.toBeVisible() | ||||
|       await page.waitForTimeout(500) | ||||
|       await page.keyboard.press('Enter') | ||||
|       await page.keyboard.press('Enter') | ||||
|       await page.keyboard.press('Enter') | ||||
|       await expect(page.locator('.cm-ghostText').first()).toBeVisible() | ||||
|       await expect(page.locator('.cm-content')).toHaveText( | ||||
|         `fn cube = (pos, scale) => {  sg = startSketchOn('XY')    |> startProfileAt(pos, %)    |> line([0, scale], %)    |> line([scale, 0], %)    |> line([0, -scale], %)  return sg}part001 = cube([0,0], 20)    |> close(%)    |> extrude(20, %)` | ||||
|       ) | ||||
|       await expect(page.locator('.cm-ghostText').first()).toHaveText( | ||||
|         `fn cube = (pos, scale) => {` | ||||
|       ) | ||||
|  | ||||
|     // Going elsewhere in the code should hide the ghost text. | ||||
|     await page.keyboard.press('Delete') | ||||
|     await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() | ||||
|       // Going elsewhere in the code should hide the ghost text. | ||||
|       await page.keyboard.press('Delete') | ||||
|       await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() | ||||
|  | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|   }) | ||||
|       await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|     }) | ||||
|  | ||||
|   test('backspace in code rejects the suggestion', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     test('backspace in code rejects the suggestion', async ({ | ||||
|       page, | ||||
|       homePage, | ||||
|     }) => { | ||||
|       const u = await getUtils(page) | ||||
|       // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|       await u.codeLocator.click() | ||||
|       await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|  | ||||
|     await expect(page.locator('.cm-ghostText')).not.toBeVisible() | ||||
|     await page.waitForTimeout(500) | ||||
|     await page.keyboard.press('Enter') | ||||
|     await page.keyboard.press('Enter') | ||||
|     await page.keyboard.press('Enter') | ||||
|     await expect(page.locator('.cm-ghostText').first()).toBeVisible() | ||||
|     await expect(page.locator('.cm-content')).toHaveText( | ||||
|       `fn cube = (pos, scale) => {  sg = startSketchOn('XY')    |> startProfileAt(pos, %)    |> line([0, scale], %)    |> line([scale, 0], %)    |> line([0, -scale], %)  return sg}part001 = cube([0,0], 20)    |> close(%)    |> extrude(20, %)` | ||||
|     ) | ||||
|     await expect(page.locator('.cm-ghostText').first()).toHaveText( | ||||
|       `fn cube = (pos, scale) => {` | ||||
|     ) | ||||
|       await expect(page.locator('.cm-ghostText')).not.toBeVisible() | ||||
|       await page.waitForTimeout(500) | ||||
|       await page.keyboard.press('Enter') | ||||
|       await page.keyboard.press('Enter') | ||||
|       await page.keyboard.press('Enter') | ||||
|       await expect(page.locator('.cm-ghostText').first()).toBeVisible() | ||||
|       await expect(page.locator('.cm-content')).toHaveText( | ||||
|         `fn cube = (pos, scale) => {  sg = startSketchOn('XY')    |> startProfileAt(pos, %)    |> line([0, scale], %)    |> line([scale, 0], %)    |> line([0, -scale], %)  return sg}part001 = cube([0,0], 20)    |> close(%)    |> extrude(20, %)` | ||||
|       ) | ||||
|       await expect(page.locator('.cm-ghostText').first()).toHaveText( | ||||
|         `fn cube = (pos, scale) => {` | ||||
|       ) | ||||
|  | ||||
|     // Going elsewhere in the code should hide the ghost text. | ||||
|     await page.keyboard.press('Backspace') | ||||
|     await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() | ||||
|       // Going elsewhere in the code should hide the ghost text. | ||||
|       await page.keyboard.press('Backspace') | ||||
|       await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() | ||||
|  | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|   }) | ||||
|       await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|     }) | ||||
|  | ||||
|   test('focus outside code pane rejects the suggestion', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     test('focus outside code pane rejects the suggestion', async ({ | ||||
|       page, | ||||
|       homePage, | ||||
|     }) => { | ||||
|       const u = await getUtils(page) | ||||
|       // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|       await u.codeLocator.click() | ||||
|       await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|  | ||||
|     await expect(page.locator('.cm-ghostText')).not.toBeVisible() | ||||
|     await page.waitForTimeout(500) | ||||
|     await page.keyboard.press('Enter') | ||||
|     await expect(page.locator('.cm-ghostText').first()).toBeVisible() | ||||
|     await expect(page.locator('.cm-content')).toHaveText( | ||||
|       `fn cube = (pos, scale) => {  sg = startSketchOn('XY')    |> startProfileAt(pos, %)    |> line([0, scale], %)    |> line([scale, 0], %)    |> line([0, -scale], %)  return sg}part001 = cube([0,0], 20)    |> close(%)    |> extrude(20, %)` | ||||
|     ) | ||||
|     await expect(page.locator('.cm-ghostText').first()).toHaveText( | ||||
|       `fn cube = (pos, scale) => {` | ||||
|     ) | ||||
|       await expect(page.locator('.cm-ghostText')).not.toBeVisible() | ||||
|       await page.waitForTimeout(500) | ||||
|       await page.keyboard.press('Enter') | ||||
|       await expect(page.locator('.cm-ghostText').first()).toBeVisible() | ||||
|       await expect(page.locator('.cm-content')).toHaveText( | ||||
|         `fn cube = (pos, scale) => {  sg = startSketchOn('XY')    |> startProfileAt(pos, %)    |> line([0, scale], %)    |> line([scale, 0], %)    |> line([0, -scale], %)  return sg}part001 = cube([0,0], 20)    |> close(%)    |> extrude(20, %)` | ||||
|       ) | ||||
|       await expect(page.locator('.cm-ghostText').first()).toHaveText( | ||||
|         `fn cube = (pos, scale) => {` | ||||
|       ) | ||||
|  | ||||
|     // Going outside the editor should hide the ghost text. | ||||
|     await page.mouse.move(0, 0) | ||||
|     await page | ||||
|       .getByRole('button', { name: 'Start Sketch' }) | ||||
|       .waitFor({ state: 'visible' }) | ||||
|     await page.getByRole('button', { name: 'Start Sketch' }).click() | ||||
|     await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() | ||||
|       // Going outside the editor should hide the ghost text. | ||||
|       await page.mouse.move(0, 0) | ||||
|       await page | ||||
|         .getByRole('button', { name: 'Start Sketch' }) | ||||
|         .waitFor({ state: 'visible' }) | ||||
|       await page.getByRole('button', { name: 'Start Sketch' }).click() | ||||
|       await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() | ||||
|  | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|       await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -1,14 +1,6 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { test, expect } from './zoo-test' | ||||
|  | ||||
| import { getUtils, setup, tearDown } from './test-utils' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }, testInfo) => { | ||||
|   await setup(context, page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
| import { getUtils } from './test-utils' | ||||
|  | ||||
| function countNewlines(input: string): number { | ||||
|   let count = 0 | ||||
| @ -24,13 +16,14 @@ test.describe('Debug pane', () => { | ||||
|   test('Artifact IDs in the artifact graph are stable across code edits', async ({ | ||||
|     page, | ||||
|     context, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const code = `sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([0, 0], %) | ||||
| |> line([1, 1], %) | ||||
| ` | ||||
|     |> startProfileAt([0, 0], %) | ||||
|   |> line([1, 1], %) | ||||
|   ` | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     const tree = page.getByTestId('debug-feature-tree') | ||||
|     const segment = tree.locator('li', { | ||||
| @ -39,7 +32,7 @@ test.describe('Debug pane', () => { | ||||
|     }) | ||||
|  | ||||
|     await test.step('Test setup', async () => { | ||||
|       await u.waitForAuthSkipAppStart() | ||||
|       await homePage.goToModelingScene() | ||||
|       await u.openKclCodePanel() | ||||
|       await u.openDebugPanel() | ||||
|       // Set the code in the code editor. | ||||
|  | ||||
| @ -1,39 +1,31 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { join } from 'path' | ||||
| import { test, expect } from './zoo-test' | ||||
| import path from 'path' | ||||
| import { | ||||
|   getUtils, | ||||
|   setupElectron, | ||||
|   tearDown, | ||||
|   executorInputPath, | ||||
|   getPlaywrightDownloadDir, | ||||
| } from './test-utils' | ||||
| import fsp from 'fs/promises' | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
|  | ||||
| test( | ||||
|   'export works on the first try', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         const bracketDir = join(dir, 'bracket') | ||||
|         await Promise.all([fsp.mkdir(bracketDir, { recursive: true })]) | ||||
|         await Promise.all([ | ||||
|           fsp.copyFile( | ||||
|             executorInputPath('router-template-slate.kcl'), | ||||
|             join(bracketDir, 'other.kcl') | ||||
|           ), | ||||
|           fsp.copyFile( | ||||
|             executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|             join(bracketDir, 'main.kcl') | ||||
|           ), | ||||
|         ]) | ||||
|       }, | ||||
|   async ({ page, context }, testInfo) => { | ||||
|     await context.folderSetupFn(async (dir) => { | ||||
|       const bracketDir = path.join(dir, 'bracket') | ||||
|       await Promise.all([fsp.mkdir(bracketDir, { recursive: true })]) | ||||
|       await Promise.all([ | ||||
|         fsp.copyFile( | ||||
|           executorInputPath('router-template-slate.kcl'), | ||||
|           path.join(bracketDir, 'other.kcl') | ||||
|         ), | ||||
|         fsp.copyFile( | ||||
|           executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|           path.join(bracketDir, 'main.kcl') | ||||
|         ), | ||||
|       ]) | ||||
|     }) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     page.on('console', console.log) | ||||
|  | ||||
| @ -93,12 +85,16 @@ test( | ||||
|       await expect(successToastMessage).toBeVisible() | ||||
|       await expect(exportingToastMessage).not.toBeVisible() | ||||
|  | ||||
|       const firstFileFullPath = path.resolve( | ||||
|         getPlaywrightDownloadDir(page), | ||||
|         exportFileName | ||||
|       ) | ||||
|       await test.step('Check the export size', async () => { | ||||
|         await expect | ||||
|           .poll( | ||||
|             async () => { | ||||
|               try { | ||||
|                 const outputGltf = await fsp.readFile(exportFileName) | ||||
|                 const outputGltf = await fsp.readFile(firstFileFullPath) | ||||
|                 return outputGltf.byteLength | ||||
|               } catch (e) { | ||||
|                 return 0 | ||||
| @ -107,9 +103,6 @@ test( | ||||
|             { timeout: 15_000 } | ||||
|           ) | ||||
|           .toBeGreaterThan(300_000) | ||||
|  | ||||
|         // clean up exported file | ||||
|         await fsp.rm(exportFileName) | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
| @ -170,12 +163,16 @@ test( | ||||
|           expect(exportingToastMessage).not.toBeVisible(), | ||||
|         ])) | ||||
|  | ||||
|       const secondFileFullPath = path.resolve( | ||||
|         getPlaywrightDownloadDir(page), | ||||
|         exportFileName | ||||
|       ) | ||||
|       await test.step('Check the export size', async () => { | ||||
|         await expect | ||||
|           .poll( | ||||
|             async () => { | ||||
|               try { | ||||
|                 const outputGltf = await fsp.readFile(exportFileName) | ||||
|                 const outputGltf = await fsp.readFile(secondFileFullPath) | ||||
|                 return outputGltf.byteLength | ||||
|               } catch (e) { | ||||
|                 return 0 | ||||
| @ -184,13 +181,7 @@ test( | ||||
|             { timeout: 15_000 } | ||||
|           ) | ||||
|           .toBeGreaterThan(100_000) | ||||
|  | ||||
|         // clean up exported file | ||||
|         await fsp.rm(exportFileName) | ||||
|       }) | ||||
|       await electronApp.close() | ||||
|     }) | ||||
|  | ||||
|     await electronApp.close() | ||||
|   } | ||||
| ) | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { test, expect } from './zoo-test' | ||||
| import fsp from 'fs/promises' | ||||
| import { uuidv4 } from 'lib/utils' | ||||
| import { | ||||
| @ -6,37 +6,27 @@ import { | ||||
|   darkModePlaneColorXZ, | ||||
|   executorInputPath, | ||||
|   getUtils, | ||||
|   setup, | ||||
|   setupElectron, | ||||
|   tearDown, | ||||
| } from './test-utils' | ||||
|  | ||||
| import { join } from 'path' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }, testInfo) => { | ||||
|   await setup(context, page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.describe('Editor tests', () => { | ||||
|   test('can comment out code with ctrl+/', async ({ page }) => { | ||||
|   test('can comment out code with ctrl+/', async ({ page, homePage }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // check no error to begin with | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.type(`sketch001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     |> close(%)`) | ||||
|  | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.press('/') | ||||
| @ -44,11 +34,11 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XY') | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     // |> close(%)`) | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   // |> close(%)`) | ||||
|  | ||||
|     // uncomment the code | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
| @ -57,23 +47,22 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XY') | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     |> close(%)`) | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|   }) | ||||
|  | ||||
|   test('if you click the format button it formats your code', async ({ | ||||
|   test('ensure we use the cache, and do not re-execute', async ({ | ||||
|     homePage, | ||||
|     page, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     // check no error to begin with | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.waitForPageLoad() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.type(`sketch001 = startSketchOn('XY') | ||||
| @ -82,36 +71,141 @@ test.describe('Editor tests', () => { | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|     await page.locator('#code-pane button:first-child').click() | ||||
|     await page.locator('button:has-text("Format code")').click() | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XY') | ||||
|     // Ensure we execute the first time. | ||||
|     await u.openDebugPanel() | ||||
|     await expect( | ||||
|       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||
|     ).toHaveCount(1) | ||||
|     await expect( | ||||
|       page.locator('[data-message-type="execution-done"]') | ||||
|     ).toHaveCount(2) | ||||
|  | ||||
|     // Add whitespace to the end of the code. | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.press('ArrowUp') | ||||
|     await page.keyboard.press('ArrowUp') | ||||
|     await page.keyboard.press('ArrowUp') | ||||
|     await page.keyboard.press('ArrowUp') | ||||
|     await page.keyboard.press('Home') | ||||
|     await page.keyboard.type('    ') | ||||
|     await page.keyboard.press('Enter') | ||||
|     await page.keyboard.type('    ') | ||||
|  | ||||
|     // Ensure we don't execute the second time. | ||||
|     await u.openDebugPanel() | ||||
|     // Make sure we didn't clear the scene. | ||||
|     await expect( | ||||
|       page.locator('[data-message-type="execution-done"]') | ||||
|     ).toHaveCount(3) | ||||
|     await expect( | ||||
|       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||
|     ).toHaveCount(1) | ||||
|   }) | ||||
|  | ||||
|   test('ensure we use the cache, and do not clear on append', async ({ | ||||
|     homePage, | ||||
|     page, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.waitForPageLoad() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.type(`sketch001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|  | ||||
|     // Ensure we execute the first time. | ||||
|     await u.openDebugPanel() | ||||
|     await expect( | ||||
|       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||
|     ).toHaveCount(1) | ||||
|     await expect( | ||||
|       page.locator('[data-message-type="execution-done"]') | ||||
|     ).toHaveCount(2) | ||||
|  | ||||
|     // Add whitespace to the end of the code. | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('End') | ||||
|     await page.keyboard.press('Enter') | ||||
|     await page.keyboard.press('Enter') | ||||
|     await page.keyboard.type('const x = 1') | ||||
|     await page.keyboard.press('Enter') | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await expect( | ||||
|       page.locator('[data-message-type="execution-done"]') | ||||
|     ).toHaveCount(3) | ||||
|     await expect( | ||||
|       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||
|     ).toHaveCount(1) | ||||
|   }) | ||||
|  | ||||
|   test('if you click the format button it formats your code', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // check no error to begin with | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.type(`sketch001 = startSketchOn('XY') | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     |> close(%)`) | ||||
|     await page.locator('#code-pane button:first-child').click() | ||||
|     await page.locator('button:has-text("Format code")').click() | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|   }) | ||||
|  | ||||
|   test('if you click the format button it formats your code and executes so lints are still there', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // check no error to begin with | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.type(`sketch_001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     |> close(%)`) | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
| @ -135,11 +229,11 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch_001 = startSketchOn('XY') | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     |> close(%)`) | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|  | ||||
|     // error in guter | ||||
|     await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() | ||||
| @ -151,29 +245,27 @@ test.describe('Editor tests', () => { | ||||
|     ).toBeVisible() | ||||
|   }) | ||||
|  | ||||
|   test('fold gutters work', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|   test('fold gutters work', async ({ page, homePage }) => { | ||||
|     const fullCode = `sketch001 = startSketchOn('XY') | ||||
|      |> startProfileAt([-10, -10], %) | ||||
|      |> line([20, 0], %) | ||||
|      |> line([0, 20], %) | ||||
|      |> line([-20, 0], %) | ||||
|      |> close(%)` | ||||
|    |> startProfileAt([-10, -10], %) | ||||
|    |> line([20, 0], %) | ||||
|    |> line([0, 20], %) | ||||
|    |> line([-20, 0], %) | ||||
|    |> close(%)` | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch001 = startSketchOn('XY') | ||||
|      |> startProfileAt([-10, -10], %) | ||||
|      |> line([20, 0], %) | ||||
|      |> line([0, 20], %) | ||||
|      |> line([-20, 0], %) | ||||
|      |> close(%)` | ||||
|    |> startProfileAt([-10, -10], %) | ||||
|    |> line([20, 0], %) | ||||
|    |> line([0, 20], %) | ||||
|    |> line([-20, 0], %) | ||||
|    |> close(%)` | ||||
|       ) | ||||
|     }) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // TODO: Jess needs to fix this but you have to mod the code to get them to show | ||||
|     // up, its an annoying codemirror thing. | ||||
| @ -224,22 +316,25 @@ test.describe('Editor tests', () => { | ||||
|     await expect(foldGutterFoldLine).not.toBeVisible() | ||||
|   }) | ||||
|  | ||||
|   test('hover over functions shows function description', async ({ page }) => { | ||||
|   test('hover over functions shows function description', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)` | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     |> close(%)` | ||||
|       ) | ||||
|     }) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // check no error to begin with | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
| @ -268,23 +363,24 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|   test('if you use the format keyboard binding it formats your code', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)` | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     |> close(%)` | ||||
|       ) | ||||
|       localStorage.setItem('disableAxis', 'true') | ||||
|     }) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // check no error to begin with | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
| @ -301,32 +397,33 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XY') | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     |> close(%)`) | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|   }) | ||||
|  | ||||
|   test('if you use the format keyboard binding it formats your code and executes so lints are shown', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch_001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)` | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     |> close(%)` | ||||
|       ) | ||||
|       localStorage.setItem('disableAxis', 'true') | ||||
|     }) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
| @ -353,11 +450,11 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch_001 = startSketchOn('XY') | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     |> close(%)`) | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|  | ||||
|     // error in guter | ||||
|     await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() | ||||
| @ -369,11 +466,14 @@ test.describe('Editor tests', () => { | ||||
|     ).toBeVisible() | ||||
|   }) | ||||
|  | ||||
|   test('if you write kcl with lint errors you get lints', async ({ page }) => { | ||||
|   test('if you write kcl with lint errors you get lints', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // check no error to begin with | ||||
|     await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible() | ||||
| @ -409,23 +509,26 @@ test.describe('Editor tests', () => { | ||||
|     await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible() | ||||
|   }) | ||||
|  | ||||
|   test('if you fixup kcl errors you clear lints', async ({ page }) => { | ||||
|   test('if you fixup kcl errors you clear lints', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([3.29, 7.86], %) | ||||
|   |> line([2.48, 2.44], %) | ||||
|   |> line([2.66, 1.17], %) | ||||
|   |> close(%) | ||||
|   ` | ||||
|     |> startProfileAt([3.29, 7.86], %) | ||||
|     |> line([2.48, 2.44], %) | ||||
|     |> line([2.66, 1.17], %) | ||||
|     |> close(%) | ||||
|     ` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // check no error to begin with | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
| @ -447,22 +550,27 @@ test.describe('Editor tests', () => { | ||||
|     ).not.toBeVisible() | ||||
|   }) | ||||
|  | ||||
|   test('if you write invalid kcl you get inlined errors', async ({ page }) => { | ||||
|   test('if you write invalid kcl you get inlined errors', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // check no error to begin with | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
|  | ||||
|     /* add the following code to the editor ($ error is not a valid line) | ||||
|       $ error | ||||
|       const topAng = 30 | ||||
|       const bottomAng = 25 | ||||
|      */ | ||||
|     /* add the following code to the editor (~ error is not a valid line) | ||||
|       * the old check here used $ but this is for tags so it changed meaning. | ||||
|       * hopefully ~ doesn't change meaning | ||||
|     ~ error | ||||
|     const topAng = 30 | ||||
|     const bottomAng = 25 | ||||
|    */ | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.type('$ error') | ||||
|     await page.keyboard.type('~ error') | ||||
|  | ||||
|     // press arrows to clear autocomplete | ||||
|     await page.keyboard.press('ArrowLeft') | ||||
| @ -479,10 +587,12 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|     // error text on hover | ||||
|     await page.hover('.cm-lint-marker-error') | ||||
|     await expect(page.getByText('Unexpected token: $').first()).toBeVisible() | ||||
|     await expect( | ||||
|       page.getByText("found unknown token '~'").first() | ||||
|     ).toBeVisible() | ||||
|  | ||||
|     // select the line that's causing the error and delete it | ||||
|     await page.getByText('$ error').click() | ||||
|     await page.getByText('~ error').click() | ||||
|     await page.keyboard.press('End') | ||||
|     await page.keyboard.down('Shift') | ||||
|     await page.keyboard.press('Home') | ||||
| @ -518,103 +628,108 @@ test.describe('Editor tests', () => { | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
|   }) | ||||
|  | ||||
|   test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `length = .750 | ||||
|   width = 0.500 | ||||
|   height = 0.500 | ||||
|   dia = 4 | ||||
|   test.fixme( | ||||
|     'error with 2 source ranges gets 2 diagnostics', | ||||
|     async ({ page, homePage }) => { | ||||
|       const u = await getUtils(page) | ||||
|       await page.addInitScript(async () => { | ||||
|         localStorage.setItem( | ||||
|           'persistCode', | ||||
|           `length = .750 | ||||
|     width = 0.500 | ||||
|     height = 0.500 | ||||
|     dia = 4 | ||||
|    | ||||
|     fn squareHole = (l, w) => { | ||||
|   squareHoleSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([-width / 2, -length / 2], %) | ||||
|   |> lineTo([width / 2, -length / 2], %) | ||||
|   |> lineTo([width / 2, length / 2], %) | ||||
|   |> lineTo([-width / 2, length / 2], %) | ||||
|   |> close(%) | ||||
|   return squareHoleSketch | ||||
|     } | ||||
|     ` | ||||
|         ) | ||||
|       }) | ||||
|       await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|   fn squareHole = (l, w) => { | ||||
|     squareHoleSketch = startSketchOn('XY') | ||||
|     |> startProfileAt([-width / 2, -length / 2], %) | ||||
|     |> lineTo([width / 2, -length / 2], %) | ||||
|     |> lineTo([width / 2, length / 2], %) | ||||
|     |> lineTo([-width / 2, length / 2], %) | ||||
|     |> close(%) | ||||
|     return squareHoleSketch | ||||
|   } | ||||
|   ` | ||||
|       ) | ||||
|     }) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|       await homePage.goToModelingScene() | ||||
|       await u.waitForPageLoad() | ||||
|       await page.waitForTimeout(1000) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|       await u.openDebugPanel() | ||||
|       await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|       await u.closeDebugPanel() | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.closeDebugPanel() | ||||
|       // check no error to begin with | ||||
|       await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
|  | ||||
|     // check no error to begin with | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
|       // Click on the bottom of the code editor to add a new line | ||||
|       await u.codeLocator.click() | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.keyboard.press('Enter') | ||||
|       await page.keyboard.type(`extrusion = startSketchOn('XY') | ||||
|   |> circle({ center: [0, 0], radius: dia/2 }, %) | ||||
|     |> hole(squareHole(length, width, height), %) | ||||
|     |> extrude(height, %)`) | ||||
|  | ||||
|     // Click on the bottom of the code editor to add a new line | ||||
|     await u.codeLocator.click() | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await page.keyboard.press('Enter') | ||||
|     await page.keyboard.type(`extrusion = startSketchOn('XY') | ||||
|     |> circle({ center = [0, 0], radius = dia/2 }, %) | ||||
|   |> hole(squareHole(length, width, height), %) | ||||
|   |> extrude(height, %)`) | ||||
|       // error in gutter | ||||
|       await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() | ||||
|       await page.hover('.cm-lint-marker-error:first-child') | ||||
|       await expect( | ||||
|         page.getByText('Expected 2 arguments, got 3').first() | ||||
|       ).toBeVisible() | ||||
|  | ||||
|     // error in gutter | ||||
|     await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() | ||||
|     await page.hover('.cm-lint-marker-error:first-child') | ||||
|     await expect( | ||||
|       page.getByText('Expected 2 arguments, got 3').first() | ||||
|     ).toBeVisible() | ||||
|  | ||||
|     // Make sure there are two diagnostics | ||||
|     await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2) | ||||
|   }) | ||||
|       // Make sure there are two diagnostics | ||||
|       await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2) | ||||
|     } | ||||
|   ) | ||||
|   test('if your kcl gets an error from the engine it is inlined', async ({ | ||||
|     context, | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|     await context.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `box = startSketchOn('XY') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([0, 10], %) | ||||
|   |> line([10, 0], %) | ||||
|   |> line([0, -10], %, $revolveAxis) | ||||
|   |> close(%) | ||||
|   |> extrude(10, %) | ||||
|  | ||||
|   sketch001 = startSketchOn(box, revolveAxis) | ||||
|   |> startProfileAt([5, 10], %) | ||||
|   |> line([0, -10], %) | ||||
|   |> line([2, 0], %) | ||||
|   |> line([0, -10], %) | ||||
|   |> close(%) | ||||
|   |> revolve({ | ||||
|   axis = revolveAxis, | ||||
|   angle = 90 | ||||
|   }, %) | ||||
|       ` | ||||
|     |> startProfileAt([0, 0], %) | ||||
|     |> line([0, 10], %) | ||||
|     |> line([10, 0], %) | ||||
|     |> line([0, -10], %, $revolveAxis) | ||||
|     |> close(%) | ||||
|     |> extrude(10, %) | ||||
|    | ||||
|     sketch001 = startSketchOn(box, revolveAxis) | ||||
|     |> startProfileAt([5, 10], %) | ||||
|     |> line([0, -10], %) | ||||
|     |> line([2, 0], %) | ||||
|     |> line([0, -10], %) | ||||
|     |> close(%) | ||||
|     |> revolve({ | ||||
|     axis: revolveAxis, | ||||
|     angle: 90 | ||||
|     }, %) | ||||
|     ` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await page.goto('/') | ||||
|     await u.waitForPageLoad() | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     await expect(page.locator('.cm-lint-marker-error')).toBeVisible() | ||||
|  | ||||
| @ -625,12 +740,15 @@ test.describe('Editor tests', () => { | ||||
|     await expect(page.getByText(searchText)).toBeVisible() | ||||
|   }) | ||||
|   test.describe('Autocomplete works', () => { | ||||
|     test('with enter/click to accept the completion', async ({ page }) => { | ||||
|     test('with enter/click to accept the completion', async ({ | ||||
|       page, | ||||
|       homePage, | ||||
|     }) => { | ||||
|       const u = await getUtils(page) | ||||
|       // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|       await u.waitForAuthSkipAppStart() | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|       // tests clicking on an option, selection the first option | ||||
|       // and arrowing down to an option | ||||
| @ -692,19 +810,19 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|       await expect(page.locator('.cm-content')) | ||||
|         .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt([3.14, 12], %) | ||||
|     |> xLine(5, %) // lin`) | ||||
|         |> startProfileAt([3.14, 12], %) | ||||
|         |> xLine(5, %) // lin`) | ||||
|  | ||||
|       // expect there to be no KCL errors | ||||
|       await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0) | ||||
|     }) | ||||
|  | ||||
|     test('with tab to accept the completion', async ({ page }) => { | ||||
|     test('with tab to accept the completion', async ({ page, homePage }) => { | ||||
|       const u = await getUtils(page) | ||||
|       // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|       await u.waitForAuthSkipAppStart() | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|       // this test might be brittle as we add and remove functions | ||||
|       // but should also be easy to update. | ||||
| @ -766,26 +884,30 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|       await expect(page.locator('.cm-content')) | ||||
|         .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt([3.14, 12], %) | ||||
|     |> xLine(5, %) // lin`) | ||||
|         |> startProfileAt([3.14, 12], %) | ||||
|         |> xLine(5, %) // lin`) | ||||
|     }) | ||||
|   }) | ||||
|   test('Can undo a click and point extrude with ctrl+z', async ({ page }) => { | ||||
|   test('Can undo a click and point extrude with ctrl+z', async ({ | ||||
|     page, | ||||
|     context, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|     await context.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt([4.61, -14.01], %) | ||||
|     |> line([12.73, -0.09], %) | ||||
|     |> tangentialArcTo([24.95, -5.38], %) | ||||
|     |> close(%)` | ||||
|   |> startProfileAt([4.61, -14.01], %) | ||||
|   |> line([12.73, -0.09], %) | ||||
|   |> tangentialArcTo([24.95, -5.38], %) | ||||
|   |> close(%)` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Start Sketch' }) | ||||
|     ).not.toBeDisabled() | ||||
| @ -838,29 +960,32 @@ test.describe('Editor tests', () => { | ||||
|     await page.waitForTimeout(100) | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt([4.61, -14.01], %) | ||||
|     |> line([12.73, -0.09], %) | ||||
|     |> tangentialArcTo([24.95, -5.38], %) | ||||
|     |> close(%)`) | ||||
|   |> startProfileAt([4.61, -14.01], %) | ||||
|   |> line([12.73, -0.09], %) | ||||
|   |> tangentialArcTo([24.95, -5.38], %) | ||||
|   |> close(%)`) | ||||
|   }) | ||||
|  | ||||
|   test('Can undo a sketch modification with ctrl+z', async ({ page }) => { | ||||
|   test('Can undo a sketch modification with ctrl+z', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt([4.61, -10.01], %) | ||||
|     |> line([12.73, -0.09], %) | ||||
|     |> tangentialArcTo([24.95, -0.38], %) | ||||
|     |> close(%) | ||||
|     |> extrude(5, %)` | ||||
|   |> startProfileAt([4.61, -10.01], %) | ||||
|   |> line([12.73, -0.09], %) | ||||
|   |> tangentialArcTo([24.95, -0.38], %) | ||||
|   |> close(%) | ||||
|   |> extrude(5, %)` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Start Sketch' }) | ||||
|     ).not.toBeDisabled() | ||||
| @ -887,7 +1012,7 @@ test.describe('Editor tests', () => { | ||||
|     }) | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     const startPX = [665, 397] | ||||
|     const startPX = [1200 / 2, 500 / 2] | ||||
|  | ||||
|     const dragPX = 40 | ||||
|  | ||||
| @ -901,9 +1026,9 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|     await expect(page.getByTestId('segment-overlay')).toHaveCount(2) | ||||
|  | ||||
|     // drag startProfieAt handle | ||||
|     // drag startProfileAt handle | ||||
|     await page.dragAndDrop('#stream', '#stream', { | ||||
|       sourcePosition: { x: startPX[0], y: startPX[1] }, | ||||
|       sourcePosition: { x: startPX[0] + 68, y: startPX[1] + 147 }, | ||||
|       targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX }, | ||||
|     }) | ||||
|     await page.waitForTimeout(100) | ||||
| @ -941,12 +1066,12 @@ test.describe('Editor tests', () => { | ||||
|     // expect the code to have changed | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([7.12, -12.68], %) | ||||
|   |> line([15.39, -2.78], %) | ||||
|   |> tangentialArcTo([27.6, -3.05], %) | ||||
|   |> close(%) | ||||
|   |> extrude(5, %) | ||||
| `) | ||||
|     |> startProfileAt([2.71, -2.71], %) | ||||
|     |> line([15.4, -2.78], %) | ||||
|     |> tangentialArcTo([27.6, -3.05], %) | ||||
|     |> close(%) | ||||
|     |> extrude(5, %) | ||||
|   `) | ||||
|  | ||||
|     // Hit undo | ||||
|     await page.keyboard.down('Control') | ||||
| @ -955,11 +1080,11 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([7.12, -12.68], %) | ||||
|   |> line([15.39, -2.78], %) | ||||
|   |> tangentialArcTo([24.95, -0.38], %) | ||||
|   |> close(%) | ||||
|   |> extrude(5, %)`) | ||||
|     |> startProfileAt([2.71, -2.71], %) | ||||
|     |> line([15.4, -2.78], %) | ||||
|     |> tangentialArcTo([24.95, -0.38], %) | ||||
|     |> close(%) | ||||
|     |> extrude(5, %)`) | ||||
|  | ||||
|     // Hit undo again. | ||||
|     await page.keyboard.down('Control') | ||||
| @ -968,12 +1093,12 @@ test.describe('Editor tests', () => { | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([7.12, -12.68], %) | ||||
|   |> line([12.73, -0.09], %) | ||||
|   |> tangentialArcTo([24.95, -0.38], %) | ||||
|   |> close(%) | ||||
|   |> extrude(5, %) | ||||
| `) | ||||
|     |> startProfileAt([2.71, -2.71], %) | ||||
|     |> line([12.73, -0.09], %) | ||||
|     |> tangentialArcTo([24.95, -0.38], %) | ||||
|     |> close(%) | ||||
|     |> extrude(5, %) | ||||
|   `) | ||||
|  | ||||
|     // Hit undo again. | ||||
|     await page.keyboard.down('Control') | ||||
| @ -983,31 +1108,29 @@ test.describe('Editor tests', () => { | ||||
|     await page.waitForTimeout(100) | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||
|     |> startProfileAt([4.61, -10.01], %) | ||||
|     |> line([12.73, -0.09], %) | ||||
|     |> tangentialArcTo([24.95, -0.38], %) | ||||
|     |> close(%) | ||||
|     |> extrude(5, %)`) | ||||
|   |> startProfileAt([4.61, -10.01], %) | ||||
|   |> line([12.73, -0.09], %) | ||||
|   |> tangentialArcTo([24.95, -0.38], %) | ||||
|   |> close(%) | ||||
|   |> extrude(5, %)`) | ||||
|   }) | ||||
|  | ||||
|   test.fixme( | ||||
|     `Can use the import stdlib function on a local OBJ file`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browserName }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async (dir) => { | ||||
|           const bracketDir = join(dir, 'cube') | ||||
|           await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|           await fsp.copyFile( | ||||
|             executorInputPath('cube.obj'), | ||||
|             join(bracketDir, 'cube.obj') | ||||
|           ) | ||||
|           await fsp.writeFile(join(bracketDir, 'main.kcl'), '') | ||||
|         }, | ||||
|     async ({ page, context }, testInfo) => { | ||||
|       await context.folderSetupFn(async (dir) => { | ||||
|         const bracketDir = join(dir, 'cube') | ||||
|         await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|         await fsp.copyFile( | ||||
|           executorInputPath('cube.obj'), | ||||
|           join(bracketDir, 'cube.obj') | ||||
|         ) | ||||
|         await fsp.writeFile(join(bracketDir, 'main.kcl'), '') | ||||
|       }) | ||||
|  | ||||
|       const viewportSize = { width: 1200, height: 500 } | ||||
|       await page.setViewportSize(viewportSize) | ||||
|       await page.setBodyDimensions(viewportSize) | ||||
|  | ||||
|       // Locators and constants | ||||
|       const u = await getUtils(page) | ||||
| @ -1065,8 +1188,6 @@ test.describe('Editor tests', () => { | ||||
|           }) | ||||
|           .toBeGreaterThan(15) | ||||
|       }) | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
| }) | ||||
|  | ||||
							
								
								
									
										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' | ||||
|  | ||||
| type CmdBarSerialised = | ||||
| @ -26,9 +26,11 @@ type CmdBarSerialised = | ||||
|  | ||||
| export class CmdBarFixture { | ||||
|   public page: Page | ||||
|   cmdBarOpenBtn!: Locator | ||||
|  | ||||
|   constructor(page: Page) { | ||||
|     this.page = page | ||||
|     this.cmdBarOpenBtn = page.getByTestId('command-bar-open-button') | ||||
|   } | ||||
|   reConstruct = (page: Page) => { | ||||
|     this.page = page | ||||
| @ -116,4 +118,21 @@ export class CmdBarFixture { | ||||
|       await this.page.keyboard.press('Enter') | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   openCmdBar = async (selectCmd?: 'promptToEdit') => { | ||||
|     // TODO why does this button not work in electron tests? | ||||
|     // await this.cmdBarOpenBtn.click() | ||||
|     await this.page.keyboard.down('ControlOrMeta') | ||||
|     await this.page.keyboard.press('KeyK') | ||||
|     await this.page.keyboard.up('ControlOrMeta') | ||||
|     await expect(this.page.getByPlaceholder('Search commands')).toBeVisible() | ||||
|     if (selectCmd === 'promptToEdit') { | ||||
|       const promptEditCommand = this.page.getByText( | ||||
|         'Use Zoo AI to edit your kcl' | ||||
|       ) | ||||
|       await expect(promptEditCommand.first()).toBeVisible() | ||||
|       await promptEditCommand.first().scrollIntoViewIfNeeded() | ||||
|       await promptEditCommand.first().click() | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -20,7 +20,7 @@ export class EditorFixture { | ||||
|   private diagnosticsTooltip!: Locator | ||||
|   private diagnosticsGutterIcon!: Locator | ||||
|   private codeContent!: Locator | ||||
|   private activeLine!: Locator | ||||
|   public activeLine!: Locator | ||||
|  | ||||
|   constructor(page: Page) { | ||||
|     this.page = page | ||||
| @ -29,7 +29,7 @@ export class EditorFixture { | ||||
|   reConstruct = (page: Page) => { | ||||
|     this.page = page | ||||
|  | ||||
|     this.codeContent = page.locator('.cm-content') | ||||
|     this.codeContent = page.locator('.cm-content[data-language="kcl"]') | ||||
|     this.diagnosticsTooltip = page.locator('.cm-tooltip-lint') | ||||
|     this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error') | ||||
|     this.activeLine = this.page.locator('.cm-activeLine') | ||||
| @ -54,13 +54,13 @@ export class EditorFixture { | ||||
|         } | ||||
|       } | ||||
|       if (!shouldNormalise) { | ||||
|         const expectStart = expect(this.codeContent) | ||||
|         const expectStart = expect.poll(() => this.codeContent.textContent()) | ||||
|         if (not) { | ||||
|           const result = await expectStart.not.toContainText(code, { timeout }) | ||||
|           const result = await expectStart.not.toContain(code) | ||||
|           await resetPane() | ||||
|           return result | ||||
|         } | ||||
|         const result = await expectStart.toContainText(code, { timeout }) | ||||
|         const result = await expectStart.toContain(code) | ||||
|         await resetPane() | ||||
|         return result | ||||
|       } | ||||
| @ -147,4 +147,28 @@ export class EditorFixture { | ||||
|   openPane() { | ||||
|     return openPane(this.page, this.paneButtonTestId) | ||||
|   } | ||||
|   scrollToText(text: string, placeCursor?: boolean) { | ||||
|     return this.page.evaluate( | ||||
|       (args: { text: string; placeCursor?: boolean }) => { | ||||
|         // error TS2339: Property 'docView' does not exist on type 'EditorView'. | ||||
|         // Except it does so :shrug: | ||||
|         // @ts-ignore | ||||
|         let index = window.editorManager._editorView?.docView.view.state.doc | ||||
|           .toString() | ||||
|           .indexOf(args.text) | ||||
|         window.editorManager._editorView?.focus() | ||||
|         window.editorManager._editorView?.dispatch({ | ||||
|           selection: window.EditorSelection.create([ | ||||
|             window.EditorSelection.cursor(index), | ||||
|           ]), | ||||
|           effects: [ | ||||
|             window.EditorView.scrollIntoView( | ||||
|               window.EditorSelection.range(index, index + 1) | ||||
|             ), | ||||
|           ], | ||||
|         }) | ||||
|       }, | ||||
|       { text, placeCursor } | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| import type { | ||||
|   BrowserContext, | ||||
|   ElectronApplication, | ||||
|   Page, | ||||
|   TestInfo, | ||||
|   Page, | ||||
| } from '@playwright/test' | ||||
| import { test as base } from '@playwright/test' | ||||
| import { getUtils, setup, setupElectron, tearDown } from '../test-utils' | ||||
|  | ||||
| import { getUtils, setup, setupElectron } from '../test-utils' | ||||
| import fsp from 'fs/promises' | ||||
| import { join } from 'path' | ||||
| import { CmdBarFixture } from './cmdBarFixture' | ||||
| @ -20,11 +20,13 @@ export class AuthenticatedApp { | ||||
|   public readonly page: Page | ||||
|   public readonly context: BrowserContext | ||||
|   public readonly testInfo: TestInfo | ||||
|   public readonly viewPortSize = { width: 1000, height: 500 } | ||||
|   public readonly viewPortSize = { width: 1200, height: 500 } | ||||
|   public electronApp: undefined | ElectronApplication | ||||
|   public dir: string = '' | ||||
|  | ||||
|   constructor(context: BrowserContext, page: Page, testInfo: TestInfo) { | ||||
|     this.page = page | ||||
|     this.context = context | ||||
|     this.page = page | ||||
|     this.testInfo = testInfo | ||||
|   } | ||||
|  | ||||
| @ -49,9 +51,7 @@ export class AuthenticatedApp { | ||||
|   } | ||||
| } | ||||
|  | ||||
| interface Fixtures { | ||||
|   app: AuthenticatedApp | ||||
|   tronApp: AuthenticatedTronApp | ||||
| export interface Fixtures { | ||||
|   cmdBar: CmdBarFixture | ||||
|   editor: EditorFixture | ||||
|   toolbar: ToolbarFixture | ||||
| @ -61,9 +61,11 @@ interface Fixtures { | ||||
| export class AuthenticatedTronApp { | ||||
|   public readonly _page: Page | ||||
|   public page: Page | ||||
|   public readonly context: BrowserContext | ||||
|   public context: BrowserContext | ||||
|   public readonly testInfo: TestInfo | ||||
|   public electronApp?: ElectronApplication | ||||
|   public electronApp: ElectronApplication | undefined | ||||
|   public readonly viewPortSize = { width: 1200, height: 500 } | ||||
|   public dir: string = '' | ||||
|  | ||||
|   constructor(context: BrowserContext, page: Page, testInfo: TestInfo) { | ||||
|     this._page = page | ||||
| @ -79,15 +81,22 @@ export class AuthenticatedTronApp { | ||||
|       appSettings?: Partial<SaveSettingsPayload> | ||||
|     } = { fixtures: {} } | ||||
|   ) { | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|     const { electronApp, page, context, dir } = await setupElectron({ | ||||
|       testInfo: this.testInfo, | ||||
|       folderSetupFn: arg.folderSetupFn, | ||||
|       cleanProjectDir: arg.cleanProjectDir, | ||||
|       appSettings: arg.appSettings, | ||||
|     }) | ||||
|     this.page = page | ||||
|     this.context = context | ||||
|     this.electronApp = electronApp | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     this.dir = dir | ||||
|  | ||||
|     // Easier to access throughout utils | ||||
|     this.page.dir = dir | ||||
|  | ||||
|     // Setup localStorage, addCookies, reload | ||||
|     await setup(this.context, this.page, this.testInfo) | ||||
|  | ||||
|     for (const key of unsafeTypedKeys(arg.fixtures)) { | ||||
|       const fixture = arg.fixtures[key] | ||||
| @ -110,32 +119,20 @@ export class AuthenticatedTronApp { | ||||
|     }) | ||||
| } | ||||
|  | ||||
| export const test = base.extend<Fixtures>({ | ||||
|   app: async ({ page, context }, use, testInfo) => { | ||||
|     await use(new AuthenticatedApp(context, page, testInfo)) | ||||
|   }, | ||||
|   tronApp: async ({ page, context }, use, testInfo) => { | ||||
|     await use(new AuthenticatedTronApp(context, page, testInfo)) | ||||
|   }, | ||||
|   cmdBar: async ({ page }, use) => { | ||||
| export const fixtures = { | ||||
|   cmdBar: async ({ page }: { page: Page }, use: any) => { | ||||
|     await use(new CmdBarFixture(page)) | ||||
|   }, | ||||
|   editor: async ({ page }, use) => { | ||||
|   editor: async ({ page }: { page: Page }, use: any) => { | ||||
|     await use(new EditorFixture(page)) | ||||
|   }, | ||||
|   toolbar: async ({ page }, use) => { | ||||
|   toolbar: async ({ page }: { page: Page }, use: any) => { | ||||
|     await use(new ToolbarFixture(page)) | ||||
|   }, | ||||
|   scene: async ({ page }, use) => { | ||||
|   scene: async ({ page }: { page: Page }, use: any) => { | ||||
|     await use(new SceneFixture(page)) | ||||
|   }, | ||||
|   homePage: async ({ page }, use) => { | ||||
|   homePage: async ({ page }: { page: Page }, use: any) => { | ||||
|     await use(new HomePageFixture(page)) | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
|  | ||||
| export { expect } from '@playwright/test' | ||||
| } | ||||
|  | ||||
| @ -4,7 +4,6 @@ import { expect } from '@playwright/test' | ||||
| interface ProjectCardState { | ||||
|   title: string | ||||
|   fileCount: number | ||||
|   folderCount: number | ||||
| } | ||||
|  | ||||
| interface HomePageState { | ||||
| @ -15,10 +14,14 @@ interface HomePageState { | ||||
| export class HomePageFixture { | ||||
|   public page: Page | ||||
|  | ||||
|   projectSection!: Locator | ||||
|   projectCard!: Locator | ||||
|   projectCardTitle!: Locator | ||||
|   projectCardFile!: Locator | ||||
|   projectCardFolder!: Locator | ||||
|   projectButtonNew!: Locator | ||||
|   projectButtonContinue!: Locator | ||||
|   projectTextName!: Locator | ||||
|   sortByDateBtn!: Locator | ||||
|   sortByNameBtn!: Locator | ||||
|  | ||||
| @ -29,11 +32,19 @@ export class HomePageFixture { | ||||
|   reConstruct = (page: Page) => { | ||||
|     this.page = page | ||||
|  | ||||
|     this.projectSection = this.page.getByTestId('home-section') | ||||
|  | ||||
|     this.projectCard = this.page.getByTestId('project-link') | ||||
|     this.projectCardTitle = this.page.getByTestId('project-title') | ||||
|     this.projectCardFile = this.page.getByTestId('project-file-count') | ||||
|     this.projectCardFolder = this.page.getByTestId('project-folder-count') | ||||
|  | ||||
|     this.projectButtonNew = this.page.getByTestId('home-new-file') | ||||
|     this.projectTextName = this.page.getByTestId('cmd-bar-arg-value') | ||||
|     this.projectButtonContinue = this.page.getByRole('button', { | ||||
|       name: 'Continue', | ||||
|     }) | ||||
|  | ||||
|     this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified') | ||||
|     this.sortByNameBtn = this.page.getByTestId('home-sort-by-name') | ||||
|   } | ||||
| @ -61,15 +72,13 @@ export class HomePageFixture { | ||||
|     const projectCards = await this.projectCard.all() | ||||
|     const projectCardStates: Array<ProjectCardState> = [] | ||||
|     for (const projectCard of projectCards) { | ||||
|       const [title, fileCount, folderCount] = await Promise.all([ | ||||
|       const [title, fileCount] = await Promise.all([ | ||||
|         (await projectCard.locator(this.projectCardTitle).textContent()) || '', | ||||
|         Number(await projectCard.locator(this.projectCardFile).textContent()), | ||||
|         Number(await projectCard.locator(this.projectCardFolder).textContent()), | ||||
|       ]) | ||||
|       projectCardStates.push({ | ||||
|         title: title, | ||||
|         fileCount, | ||||
|         folderCount, | ||||
|       }) | ||||
|     } | ||||
|     return projectCardStates | ||||
| @ -94,10 +103,25 @@ export class HomePageFixture { | ||||
|       .toEqual(expectedState) | ||||
|   } | ||||
|  | ||||
|   createAndGoToProject = async (projectTitle: string) => { | ||||
|     await expect(this.projectSection).not.toHaveText('Loading your Projects...') | ||||
|     await this.projectButtonNew.click() | ||||
|     await this.projectTextName.click() | ||||
|     await this.projectTextName.fill(projectTitle) | ||||
|     await this.projectButtonContinue.click() | ||||
|   } | ||||
|  | ||||
|   openProject = async (projectTitle: string) => { | ||||
|     const projectCard = this.projectCard.locator( | ||||
|       this.page.getByText(projectTitle) | ||||
|     ) | ||||
|     await projectCard.click() | ||||
|   } | ||||
|  | ||||
|   goToModelingScene = async (name: string = 'testDefault') => { | ||||
|     // On web this is a no-op. There is no project view. | ||||
|     if (process.env.PLATFORM === 'web') return | ||||
|  | ||||
|     await this.createAndGoToProject(name) | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -53,8 +53,9 @@ export class SceneFixture { | ||||
|  | ||||
|   expectState = async (expected: SceneSerialised) => { | ||||
|     return expect | ||||
|       .poll(() => this._serialiseScene(), { | ||||
|         message: `Expected scene state to match`, | ||||
|       .poll(async () => await this._serialiseScene(), { | ||||
|         intervals: [1_000, 2_000, 10_000], | ||||
|         timeout: 60000, | ||||
|       }) | ||||
|       .toEqual(expected) | ||||
|   } | ||||
| @ -187,7 +188,10 @@ export class SceneFixture { | ||||
|         type: 'default_camera_get_settings', | ||||
|       }, | ||||
|     }) | ||||
|     await this.waitForExecutionDone() | ||||
|     await this.page | ||||
|       .locator(`[data-receive-command-type="default_camera_get_settings"]`) | ||||
|       .first() | ||||
|       .waitFor() | ||||
|     const position = await Promise.all([ | ||||
|       this.page.getByTestId('cam-x-position').inputValue().then(Number), | ||||
|       this.page.getByTestId('cam-y-position').inputValue().then(Number), | ||||
| @ -214,23 +218,7 @@ export class SceneFixture { | ||||
|     coords: { x: number; y: number }, | ||||
|     diff: number | ||||
|   ) => { | ||||
|     let finalValue = colour | ||||
|     await expect | ||||
|       .poll(async () => { | ||||
|         const pixel = (await getPixelRGBs(this.page)(coords, 1))[0] | ||||
|         if (!pixel) return null | ||||
|         finalValue = pixel | ||||
|         return pixel.every( | ||||
|           (channel, index) => Math.abs(channel - colour[index]) < diff | ||||
|         ) | ||||
|       }) | ||||
|       .toBeTruthy() | ||||
|       .catch((cause) => { | ||||
|         throw new Error( | ||||
|           `ExpectPixelColor: expecting ${colour} got ${finalValue}`, | ||||
|           { cause } | ||||
|         ) | ||||
|       }) | ||||
|     await expectPixelColor(this.page, colour, coords, diff) | ||||
|   } | ||||
|  | ||||
|   get gizmo() { | ||||
| @ -238,6 +226,7 @@ export class SceneFixture { | ||||
|   } | ||||
|  | ||||
|   async clickGizmoMenuItem(name: string) { | ||||
|     await this.gizmo.hover() | ||||
|     await this.gizmo.click({ button: 'right' }) | ||||
|     const buttonToTest = this.page.getByRole('button', { | ||||
|       name: name, | ||||
| @ -246,3 +235,28 @@ export class SceneFixture { | ||||
|     await buttonToTest.click() | ||||
|   } | ||||
| } | ||||
|  | ||||
| export async function expectPixelColor( | ||||
|   page: Page, | ||||
|   colour: [number, number, number], | ||||
|   coords: { x: number; y: number }, | ||||
|   diff: number | ||||
| ) { | ||||
|   let finalValue = colour | ||||
|   await expect | ||||
|     .poll(async () => { | ||||
|       const pixel = (await getPixelRGBs(page)(coords, 1))[0] | ||||
|       if (!pixel) return null | ||||
|       finalValue = pixel | ||||
|       return pixel.every( | ||||
|         (channel, index) => Math.abs(channel - colour[index]) < diff | ||||
|       ) | ||||
|     }) | ||||
|     .toBeTruthy() | ||||
|     .catch((cause) => { | ||||
|       throw new Error( | ||||
|         `ExpectPixelColor: expecting ${colour} got ${finalValue}`, | ||||
|         { cause } | ||||
|       ) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| @ -1,11 +1,20 @@ | ||||
| import type { Page, Locator } from '@playwright/test' | ||||
| import { expect } from './fixtureSetup' | ||||
| import { doAndWaitForImageDiff } from '../test-utils' | ||||
| import { expect } from '../zoo-test' | ||||
| import { | ||||
|   checkIfPaneIsOpen, | ||||
|   closePane, | ||||
|   doAndWaitForImageDiff, | ||||
|   openPane, | ||||
| } from '../test-utils' | ||||
| import { SidebarType } from 'components/ModelingSidebar/ModelingPanes' | ||||
| import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants' | ||||
|  | ||||
| export class ToolbarFixture { | ||||
|   public page: Page | ||||
|  | ||||
|   extrudeButton!: Locator | ||||
|   loftButton!: Locator | ||||
|   shellButton!: Locator | ||||
|   offsetPlaneButton!: Locator | ||||
|   startSketchBtn!: Locator | ||||
|   lineBtn!: Locator | ||||
| @ -18,6 +27,10 @@ export class ToolbarFixture { | ||||
|   filePane!: Locator | ||||
|   exeIndicator!: Locator | ||||
|   treeInputField!: Locator | ||||
|   /** The sidebar button for the Feature Tree pane */ | ||||
|   featureTreeId = 'feature-tree' as const | ||||
|   /** The pane element for the Feature Tree */ | ||||
|   featureTreePane!: Locator | ||||
|  | ||||
|   constructor(page: Page) { | ||||
|     this.page = page | ||||
| @ -26,6 +39,8 @@ export class ToolbarFixture { | ||||
|   reConstruct = (page: Page) => { | ||||
|     this.page = page | ||||
|     this.extrudeButton = page.getByTestId('extrude') | ||||
|     this.loftButton = page.getByTestId('loft') | ||||
|     this.shellButton = page.getByTestId('shell') | ||||
|     this.offsetPlaneButton = page.getByTestId('plane-offset') | ||||
|     this.startSketchBtn = page.getByTestId('sketch') | ||||
|     this.lineBtn = page.getByTestId('line') | ||||
| @ -37,6 +52,7 @@ export class ToolbarFixture { | ||||
|     this.treeInputField = page.getByTestId('tree-input-field') | ||||
|  | ||||
|     this.filePane = page.locator('#files-pane') | ||||
|     this.featureTreePane = page.locator('#feature-tree-pane') | ||||
|     this.fileCreateToast = page.getByText('Successfully created') | ||||
|     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') | ||||
|   } | ||||
| @ -87,4 +103,76 @@ export class ToolbarFixture { | ||||
|       await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async closePane(paneId: SidebarType) { | ||||
|     return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) | ||||
|   } | ||||
|   async openPane(paneId: SidebarType) { | ||||
|     return openPane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) | ||||
|   } | ||||
|   async checkIfPaneIsOpen(paneId: SidebarType) { | ||||
|     return checkIfPaneIsOpen(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) | ||||
|   } | ||||
|  | ||||
|   async openFeatureTreePane() { | ||||
|     return this.openPane(this.featureTreeId) | ||||
|   } | ||||
|   async closeFeatureTreePane() { | ||||
|     await this.closePane(this.featureTreeId) | ||||
|   } | ||||
|   async checkIfFeatureTreePaneIsOpen() { | ||||
|     return this.checkIfPaneIsOpen(this.featureTreeId) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get a specific operation button from the Feature Tree pane | ||||
|    */ | ||||
|   async getFeatureTreeOperation(operationName: string, operationIndex: number) { | ||||
|     await this.openFeatureTreePane() | ||||
|     await expect(this.featureTreePane).toBeVisible() | ||||
|     return this.featureTreePane | ||||
|       .getByRole('button', { | ||||
|         name: operationName, | ||||
|       }) | ||||
|       .nth(operationIndex) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * View source on a specific operation in the Feature Tree pane. | ||||
|    * @param operationName The name of the operation type | ||||
|    * @param operationIndex The index out of operations of this type | ||||
|    */ | ||||
|   async viewSourceOnOperation(operationName: string, operationIndex: number) { | ||||
|     const operationButton = await this.getFeatureTreeOperation( | ||||
|       operationName, | ||||
|       operationIndex | ||||
|     ) | ||||
|     const viewSourceMenuButton = this.page.getByRole('button', { | ||||
|       name: 'View KCL source code', | ||||
|     }) | ||||
|  | ||||
|     await operationButton.click({ button: 'right' }) | ||||
|     await expect(viewSourceMenuButton).toBeVisible() | ||||
|     await viewSourceMenuButton.click() | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Go to definition on a specific operation in the Feature Tree pane | ||||
|    */ | ||||
|   async goToDefinitionOnOperation( | ||||
|     operationName: string, | ||||
|     operationIndex: number | ||||
|   ) { | ||||
|     const operationButton = await this.getFeatureTreeOperation( | ||||
|       operationName, | ||||
|       operationIndex | ||||
|     ) | ||||
|     const goToDefinitionMenuButton = this.page.getByRole('button', { | ||||
|       name: 'View function definition', | ||||
|     }) | ||||
|  | ||||
|     await operationButton.click({ button: 'right' }) | ||||
|     await expect(goToDefinitionMenuButton).toBeVisible() | ||||
|     await goToDefinitionMenuButton.click() | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,29 +1,22 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { setupElectron, tearDown, executorInputPath } from './test-utils' | ||||
| import { test, expect } from './zoo-test' | ||||
| import { executorInputPath } from './test-utils' | ||||
| import { join } from 'path' | ||||
| import fsp from 'fs/promises' | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
|  | ||||
| test( | ||||
|   'When machine-api server not found butt is disabled and shows the reason', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         const bracketDir = join(dir, 'bracket') | ||||
|         await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|         await fsp.copyFile( | ||||
|           executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|           join(bracketDir, 'main.kcl') | ||||
|         ) | ||||
|       }, | ||||
|   async ({ context, page }, testInfo) => { | ||||
|     await context.folderSetupFn(async (dir) => { | ||||
|       const bracketDir = join(dir, 'bracket') | ||||
|       await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|       await fsp.copyFile( | ||||
|         executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|         join(bracketDir, 'main.kcl') | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await expect(page.getByText('bracket')).toBeVisible() | ||||
|  | ||||
| @ -47,28 +40,23 @@ test( | ||||
|     // that the machine-api server is not found | ||||
|     await makeButton.hover() | ||||
|     await expect(page.getByText(notFoundText).first()).toBeVisible() | ||||
|  | ||||
|     await electronApp.close() | ||||
|   } | ||||
| ) | ||||
|  | ||||
| test( | ||||
|   'When machine-api server not found home screen & project status shows the reason', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         const bracketDir = join(dir, 'bracket') | ||||
|         await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|         await fsp.copyFile( | ||||
|           executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|           join(bracketDir, 'main.kcl') | ||||
|         ) | ||||
|       }, | ||||
|   async ({ context, page }, testInfo) => { | ||||
|     await context.folderSetupFn(async (dir) => { | ||||
|       const bracketDir = join(dir, 'bracket') | ||||
|       await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|       await fsp.copyFile( | ||||
|         executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|         join(bracketDir, 'main.kcl') | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|     const notFoundText = 'Machine API server was not discovered' | ||||
|  | ||||
| @ -91,7 +79,5 @@ test( | ||||
|  | ||||
|     await networkMachineToggle.hover() | ||||
|     await expect(page.getByText(notFoundText).nth(1)).toBeVisible() | ||||
|  | ||||
|     await electronApp.close() | ||||
|   } | ||||
| ) | ||||
|  | ||||
							
								
								
									
										12
									
								
								e2e/playwright/null.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | ||||
| // These tests are meant to simply test starting and stopping the electron | ||||
| // application, check it can make it to the project pane, and nothing more. | ||||
| // It also tests our test wrappers are working. | ||||
| // Additionally this serves as a nice minimal example. | ||||
|  | ||||
| import { test, expect } from './zoo-test' | ||||
|  | ||||
| test.describe('Open the application', () => { | ||||
|   test('see the project view', async ({ page, context }) => { | ||||
|     await expect(page.getByTestId('home-section')).toBeVisible() | ||||
|   }) | ||||
| }) | ||||
| @ -1,79 +1,78 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { test, expect } from './zoo-test' | ||||
| import { join } from 'path' | ||||
| import fsp from 'fs/promises' | ||||
| import { | ||||
|   getUtils, | ||||
|   setup, | ||||
|   setupElectron, | ||||
|   tearDown, | ||||
|   executorInputPath, | ||||
|   createProject, | ||||
| } from './test-utils' | ||||
| import { getUtils, executorInputPath, createProject } from './test-utils' | ||||
| import { bracket } from 'lib/exampleKcl' | ||||
| import { onboardingPaths } from 'routes/Onboarding/paths' | ||||
| import { | ||||
|   TEST_SETTINGS_KEY, | ||||
|   TEST_SETTINGS_ONBOARDING_START, | ||||
|   TEST_SETTINGS_ONBOARDING_EXPORT, | ||||
|   TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING, | ||||
|   TEST_SETTINGS_ONBOARDING_USER_MENU, | ||||
| } from './storageStates' | ||||
| import * as TOML from '@iarna/toml' | ||||
| import { expectPixelColor } from './fixtures/sceneFixture' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }, testInfo) => { | ||||
|   if (testInfo.tags.includes('@electron')) { | ||||
|     return | ||||
|   } | ||||
|   await setup(context, page) | ||||
| }) | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
| // Because onboarding relies on an app setting we need to set it as incompletel | ||||
| // for all these tests. | ||||
|  | ||||
| test.describe('Onboarding tests', () => { | ||||
|   test('Onboarding code is shown in the editor', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|     // Override beforeEach test setup | ||||
|     await page.addInitScript( | ||||
|       async ({ settingsKey }) => { | ||||
|         // Give no initial code, so that the onboarding start is shown immediately | ||||
|         localStorage.removeItem('persistCode') | ||||
|         localStorage.removeItem(settingsKey) | ||||
|   test( | ||||
|     'Onboarding code is shown in the editor', | ||||
|     { | ||||
|       appSettings: { | ||||
|         app: { | ||||
|           onboardingStatus: 'incomplete', | ||||
|         }, | ||||
|       }, | ||||
|       { settingsKey: TEST_SETTINGS_KEY } | ||||
|     ) | ||||
|       cleanProjectDir: true, | ||||
|     }, | ||||
|     async ({ context, page, homePage }) => { | ||||
|       const u = await getUtils(page) | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       // Test that the onboarding pane loaded | ||||
|       await expect( | ||||
|         page.getByText('Welcome to Modeling App! This') | ||||
|       ).toBeVisible() | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|       // Test that the onboarding pane loaded | ||||
|       await expect( | ||||
|         page.getByText('Welcome to Modeling App! This') | ||||
|       ).toBeVisible() | ||||
|  | ||||
|     // Test that the onboarding pane loaded | ||||
|     await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible() | ||||
|       // *and* that the code is shown in the editor | ||||
|       await expect(page.locator('.cm-content')).toContainText( | ||||
|         '// Shelf Bracket' | ||||
|       ) | ||||
|  | ||||
|     // *and* that the code is shown in the editor | ||||
|     await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket') | ||||
|   }) | ||||
|       // Make sure the model loaded | ||||
|       const XYPlanePoint = { x: 774, y: 116 } as const | ||||
|       const modelColor: [number, number, number] = [45, 45, 45] | ||||
|       await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) | ||||
|       expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan( | ||||
|         8 | ||||
|       ) | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     'Desktop: fresh onboarding executes and loads', | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browserName: _ }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         appSettings: { | ||||
|           app: { | ||||
|             onboardingStatus: 'incomplete', | ||||
|           }, | ||||
|     { | ||||
|       tag: '@electron', | ||||
|       appSettings: { | ||||
|         app: { | ||||
|           onboardingStatus: 'incomplete', | ||||
|         }, | ||||
|         cleanProjectDir: true, | ||||
|       }) | ||||
|  | ||||
|       }, | ||||
|       cleanProjectDir: true, | ||||
|     }, | ||||
|     async ({ page, homePage }, testInfo) => { | ||||
|       const u = await getUtils(page) | ||||
|  | ||||
|       const viewportSize = { width: 1200, height: 500 } | ||||
|       await page.setViewportSize(viewportSize) | ||||
|       await page.setBodyDimensions(viewportSize) | ||||
|  | ||||
|       await test.step(`Create a project and open to the onboarding`, async () => { | ||||
|         await createProject({ name: 'project-link', page }) | ||||
| @ -92,321 +91,370 @@ test.describe('Onboarding tests', () => { | ||||
|         await expect(page.locator('.cm-content')).toContainText( | ||||
|           '// Shelf Bracket' | ||||
|         ) | ||||
|       }) | ||||
|  | ||||
|       await electronApp.close() | ||||
|         // TODO: jess make less shit | ||||
|         // Make sure the model loaded | ||||
|         //const XYPlanePoint = { x: 986, y: 522 } as const | ||||
|         //const modelColor: [number, number, number] = [76, 76, 76] | ||||
|         //await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) | ||||
|  | ||||
|         //await expectPixelColor(page, modelColor, XYPlanePoint, 8) | ||||
|       }) | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test('Code resets after confirmation', async ({ page }) => { | ||||
|     const initialCode = `sketch001 = startSketchOn('XZ')` | ||||
|  | ||||
|     // Load the page up with some code so we see the confirmation warning | ||||
|     // when we go to replay onboarding | ||||
|     await page.addInitScript((code) => { | ||||
|       localStorage.setItem('persistCode', code) | ||||
|     }, initialCode) | ||||
|  | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     // Replay the onboarding | ||||
|     await page.getByRole('link', { name: 'Settings' }).last().click() | ||||
|     const replayButton = page.getByRole('button', { name: 'Replay onboarding' }) | ||||
|     await expect(replayButton).toBeVisible() | ||||
|     await replayButton.click() | ||||
|  | ||||
|     // Ensure we see the warning, and that the code has not yet updated | ||||
|     await expect( | ||||
|       page.getByText('Replaying onboarding resets your code') | ||||
|     ).toBeVisible() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(initialCode) | ||||
|  | ||||
|     const nextButton = page.getByTestId('onboarding-next') | ||||
|     await expect(nextButton).toBeVisible() | ||||
|     await nextButton.click() | ||||
|  | ||||
|     // Ensure we see the introduction and that the code has been reset | ||||
|     await expect(page.getByText('Welcome to Modeling App!')).toBeVisible() | ||||
|     await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket') | ||||
|  | ||||
|     // Ensure we persisted the code to local storage. | ||||
|     // Playwright's addInitScript method unfortunately will reset | ||||
|     // this code if we try reloading the page as a test, | ||||
|     // so this is our best way to test persistence afaik. | ||||
|     expect( | ||||
|       await page.evaluate(() => { | ||||
|         return localStorage.getItem('persistCode') | ||||
|       }) | ||||
|     ).toContain('// Shelf Bracket') | ||||
|   }) | ||||
|  | ||||
|   test('Click through each onboarding step', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|     // Override beforeEach test setup | ||||
|     await page.addInitScript( | ||||
|       async ({ settingsKey, settings }) => { | ||||
|         // Give no initial code, so that the onboarding start is shown immediately | ||||
|         localStorage.setItem('persistCode', '') | ||||
|         localStorage.setItem(settingsKey, settings) | ||||
|   test( | ||||
|     'Code resets after confirmation', | ||||
|     { | ||||
|       appSettings: { | ||||
|         app: { | ||||
|           onboardingStatus: 'incomplete', | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         settingsKey: TEST_SETTINGS_KEY, | ||||
|         settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_START }), | ||||
|       } | ||||
|     ) | ||||
|       cleanProjectDir: true, | ||||
|     }, | ||||
|     async ({ context, page, homePage }) => { | ||||
|       const initialCode = `sketch001 = startSketchOn('XZ')` | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 1080 }) | ||||
|       // Load the page up with some code so we see the confirmation warning | ||||
|       // when we go to replay onboarding | ||||
|       await context.addInitScript((code) => { | ||||
|         localStorage.setItem('persistCode', code) | ||||
|       }, initialCode) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|     // Test that the onboarding pane loaded | ||||
|     await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible() | ||||
|       // Replay the onboarding | ||||
|       await page.getByRole('link', { name: 'Settings' }).last().click() | ||||
|       const replayButton = page.getByRole('button', { | ||||
|         name: 'Replay onboarding', | ||||
|       }) | ||||
|       await expect(replayButton).toBeVisible() | ||||
|       await replayButton.click() | ||||
|  | ||||
|     const nextButton = page.getByTestId('onboarding-next') | ||||
|       // Ensure we see the warning, and that the code has not yet updated | ||||
|       await expect(page.getByText('Would you like to create')).toBeVisible() | ||||
|       await expect(page.locator('.cm-content')).toHaveText(initialCode) | ||||
|  | ||||
|     while ((await nextButton.innerText()) !== 'Finish') { | ||||
|       await expect(nextButton).toBeVisible() | ||||
|       const nextButton = page.getByTestId('onboarding-next') | ||||
|       await nextButton.hover() | ||||
|       await nextButton.click() | ||||
|  | ||||
|       // Ensure we see the introduction and that the code has been reset | ||||
|       await expect(page.getByText('Welcome to Modeling App!')).toBeVisible() | ||||
|       await expect(page.locator('.cm-content')).toContainText( | ||||
|         '// Shelf Bracket' | ||||
|       ) | ||||
|  | ||||
|       // There used to be old code here that checked if we stored the reset | ||||
|       // code into localStorage but that isn't the case on desktop. It gets | ||||
|       // saved to the file system, which we have other tests for. | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|     // Finish the onboarding | ||||
|     await expect(nextButton).toBeVisible() | ||||
|     await nextButton.click() | ||||
|  | ||||
|     // Test that the onboarding pane is gone | ||||
|     await expect(page.getByTestId('onboarding-content')).not.toBeVisible() | ||||
|     await expect(page.url()).not.toContain('onboarding') | ||||
|   }) | ||||
|  | ||||
|   test('Onboarding redirects and code updating', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|     // Override beforeEach test setup | ||||
|     await page.addInitScript( | ||||
|       async ({ settingsKey, settings }) => { | ||||
|         // Give some initial code, so we can test that it's cleared | ||||
|         localStorage.setItem('persistCode', 'sigmaAllow = 15000') | ||||
|         localStorage.setItem(settingsKey, settings) | ||||
|   test( | ||||
|     'Click through each onboarding step', | ||||
|     { | ||||
|       appSettings: { | ||||
|         app: { | ||||
|           onboardingStatus: 'incomplete', | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         settingsKey: TEST_SETTINGS_KEY, | ||||
|         settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_EXPORT }), | ||||
|     }, | ||||
|     async ({ context, page, homePage }) => { | ||||
|       // Override beforeEach test setup | ||||
|       await context.addInitScript( | ||||
|         async ({ settingsKey, settings }) => { | ||||
|           // Give no initial code, so that the onboarding start is shown immediately | ||||
|           localStorage.setItem('persistCode', '') | ||||
|           localStorage.setItem(settingsKey, settings) | ||||
|         }, | ||||
|         { | ||||
|           settingsKey: TEST_SETTINGS_KEY, | ||||
|           settings: TOML.stringify({ | ||||
|             settings: TEST_SETTINGS_ONBOARDING_START, | ||||
|           }), | ||||
|         } | ||||
|       ) | ||||
|  | ||||
|       await page.setBodyDimensions({ width: 1200, height: 1080 }) | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|       // Test that the onboarding pane loaded | ||||
|       await expect( | ||||
|         page.getByText('Welcome to Modeling App! This') | ||||
|       ).toBeVisible() | ||||
|  | ||||
|       const nextButton = page.getByTestId('onboarding-next') | ||||
|  | ||||
|       while ((await nextButton.innerText()) !== 'Finish') { | ||||
|         await nextButton.hover() | ||||
|         await nextButton.click() | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       // Finish the onboarding | ||||
|       await nextButton.hover() | ||||
|       await nextButton.click() | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     // Test that the redirect happened | ||||
|     await expect(page.url().split(':3000').slice(-1)[0]).toBe( | ||||
|       `/file/%2Fbrowser%2Fmain.kcl/onboarding/export` | ||||
|     ) | ||||
|  | ||||
|     // Test that you come back to this page when you refresh | ||||
|     await page.reload() | ||||
|     await expect(page.url().split(':3000').slice(-1)[0]).toBe( | ||||
|       `/file/%2Fbrowser%2Fmain.kcl/onboarding/export` | ||||
|     ) | ||||
|  | ||||
|     // Test that the onboarding pane loaded | ||||
|     const title = page.locator('[data-testid="onboarding-content"]') | ||||
|     await expect(title).toBeAttached() | ||||
|  | ||||
|     // Test that the code changes when you advance to the next step | ||||
|     await page.locator('[data-testid="onboarding-next"]').click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText('') | ||||
|  | ||||
|     // Test that the code is not empty when you click on the next step | ||||
|     await page.locator('[data-testid="onboarding-next"]').click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(/.+/) | ||||
|   }) | ||||
|  | ||||
|   test('Onboarding code gets reset to demo on Interactive Numbers step', async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     test.skip( | ||||
|       process.platform === 'darwin', | ||||
|       "Skip on macOS, because Playwright isn't behaving the same as the actual browser" | ||||
|     ) | ||||
|     const u = await getUtils(page) | ||||
|     const badCode = `// This is bad code we shouldn't see` | ||||
|     // Override beforeEach test setup | ||||
|     await page.addInitScript( | ||||
|       async ({ settingsKey, settings, badCode }) => { | ||||
|         localStorage.setItem('persistCode', badCode) | ||||
|         localStorage.setItem(settingsKey, settings) | ||||
|       }, | ||||
|       { | ||||
|         settingsKey: TEST_SETTINGS_KEY, | ||||
|         settings: TOML.stringify({ | ||||
|           settings: TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING, | ||||
|         }), | ||||
|         badCode, | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 1080 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     await page.waitForURL('**' + onboardingPaths.PARAMETRIC_MODELING, { | ||||
|       waitUntil: 'domcontentloaded', | ||||
|     }) | ||||
|  | ||||
|     const bracketNoNewLines = bracket.replace(/\n/g, '') | ||||
|  | ||||
|     // Check the code got reset on load | ||||
|     await expect(page.locator('#code-pane')).toBeVisible() | ||||
|     await expect(u.codeLocator).toHaveText(bracketNoNewLines, { | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|  | ||||
|     // Mess with the code again | ||||
|     await u.codeLocator.selectText() | ||||
|     await u.codeLocator.fill(badCode) | ||||
|     await expect(u.codeLocator).toHaveText(badCode) | ||||
|  | ||||
|     // Click to the next step | ||||
|     await page.locator('[data-testid="onboarding-next"]').click() | ||||
|     await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, { | ||||
|       waitUntil: 'domcontentloaded', | ||||
|     }) | ||||
|  | ||||
|     // Check that the code has been reset | ||||
|     await expect(u.codeLocator).toHaveText(bracketNoNewLines) | ||||
|   }) | ||||
|  | ||||
|   test('Avatar text updates depending on image load success', async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     // Override beforeEach test setup | ||||
|     await page.addInitScript( | ||||
|       async ({ settingsKey, settings }) => { | ||||
|         localStorage.setItem(settingsKey, settings) | ||||
|       }, | ||||
|       { | ||||
|         settingsKey: TEST_SETTINGS_KEY, | ||||
|         settings: TOML.stringify({ | ||||
|           settings: TEST_SETTINGS_ONBOARDING_USER_MENU, | ||||
|         }), | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' }) | ||||
|  | ||||
|     // Test that the text in this step is correct | ||||
|     const avatarLocator = await page | ||||
|       .getByTestId('user-sidebar-toggle') | ||||
|       .locator('img') | ||||
|     const onboardingOverlayLocator = await page | ||||
|       .getByTestId('onboarding-content') | ||||
|       .locator('div') | ||||
|       .nth(1) | ||||
|  | ||||
|     // Expect the avatar to be visible and for the text to reference it | ||||
|     await expect(avatarLocator).toBeVisible() | ||||
|     await expect(onboardingOverlayLocator).toBeVisible() | ||||
|     await expect(onboardingOverlayLocator).toContainText('your avatar') | ||||
|  | ||||
|     // This is to force the avatar to 404. | ||||
|     // For our test image (only triggers locally. on CI, it's Kurt's / | ||||
|     // gravatar image ) | ||||
|     await page.route('/cat.jpg', async (route) => { | ||||
|       await route.fulfill({ | ||||
|         status: 404, | ||||
|         contentType: 'text/plain', | ||||
|         body: 'Not Found!', | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     // 404 the CI avatar image | ||||
|     await page.route('https://lh3.googleusercontent.com/**', async (route) => { | ||||
|       await route.fulfill({ | ||||
|         status: 404, | ||||
|         contentType: 'text/plain', | ||||
|         body: 'Not Found!', | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     await page.reload({ waitUntil: 'domcontentloaded' }) | ||||
|  | ||||
|     // Now expect the text to be different | ||||
|     await expect(avatarLocator).not.toBeVisible() | ||||
|     await expect(onboardingOverlayLocator).toBeVisible() | ||||
|     await expect(onboardingOverlayLocator).toContainText('the menu button') | ||||
|   }) | ||||
|  | ||||
|   test("Avatar text doesn't mention avatar when no avatar", async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     // Override beforeEach test setup | ||||
|     await page.addInitScript( | ||||
|       async ({ settingsKey, settings }) => { | ||||
|         localStorage.setItem(settingsKey, settings) | ||||
|         localStorage.setItem('FORCE_NO_IMAGE', 'FORCE_NO_IMAGE') | ||||
|       }, | ||||
|       { | ||||
|         settingsKey: TEST_SETTINGS_KEY, | ||||
|         settings: TOML.stringify({ | ||||
|           settings: TEST_SETTINGS_ONBOARDING_USER_MENU, | ||||
|         }), | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' }) | ||||
|  | ||||
|     // Test that the text in this step is correct | ||||
|     const sidebar = page.getByTestId('user-sidebar-toggle') | ||||
|     const avatar = sidebar.locator('img') | ||||
|     const onboardingOverlayLocator = page | ||||
|       .getByTestId('onboarding-content') | ||||
|       .locator('div') | ||||
|       .nth(1) | ||||
|  | ||||
|     // Expect the avatar to be visible and for the text to reference it | ||||
|     await expect(avatar).not.toBeVisible() | ||||
|     await expect(onboardingOverlayLocator).toBeVisible() | ||||
|     await expect(onboardingOverlayLocator).toContainText('the menu button') | ||||
|  | ||||
|     // Test we mention what else is in this menu for https://github.com/KittyCAD/modeling-app/issues/2939 | ||||
|     // which doesn't deserver its own full test spun up | ||||
|     const userMenuFeatures = [ | ||||
|       'manage your account', | ||||
|       'report a bug', | ||||
|       'request a feature', | ||||
|       'sign out', | ||||
|     ] | ||||
|     for (const feature of userMenuFeatures) { | ||||
|       await expect(onboardingOverlayLocator).toContainText(feature) | ||||
|       // Test that the onboarding pane is gone | ||||
|       await expect(page.getByTestId('onboarding-content')).not.toBeVisible() | ||||
|       await expect.poll(() => page.url()).not.toContain('/onboarding') | ||||
|     } | ||||
|   }) | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     'Onboarding redirects and code updating', | ||||
|     { | ||||
|       appSettings: { | ||||
|         app: { | ||||
|           onboardingStatus: '/export', | ||||
|         }, | ||||
|       }, | ||||
|       cleanProjectDir: true, | ||||
|     }, | ||||
|     async ({ context, page, homePage }) => { | ||||
|       const originalCode = 'sigmaAllow = 15000' | ||||
|  | ||||
|       // Override beforeEach test setup | ||||
|       await context.addInitScript( | ||||
|         async ({ settingsKey, settings }) => { | ||||
|           // Give some initial code, so we can test that it's cleared | ||||
|           localStorage.setItem('persistCode', originalCode) | ||||
|           localStorage.setItem(settingsKey, settings) | ||||
|         }, | ||||
|         { | ||||
|           settingsKey: TEST_SETTINGS_KEY, | ||||
|           settings: TOML.stringify({ | ||||
|             settings: TEST_SETTINGS_ONBOARDING_EXPORT, | ||||
|           }), | ||||
|         } | ||||
|       ) | ||||
|  | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|       // Test that the redirect happened | ||||
|       await expect.poll(() => page.url()).toContain('/onboarding/export') | ||||
|  | ||||
|       // Test that you come back to this page when you refresh | ||||
|       await page.reload() | ||||
|       await expect.poll(() => page.url()).toContain('/onboarding/export') | ||||
|  | ||||
|       // Test that the code changes when you advance to the next step | ||||
|       await page.getByTestId('onboarding-next').hover() | ||||
|       await page.getByTestId('onboarding-next').click() | ||||
|  | ||||
|       // Test that the onboarding pane loaded | ||||
|       const title = page.locator('[data-testid="onboarding-content"]') | ||||
|       await expect(title).toBeAttached() | ||||
|  | ||||
|       await expect(page.locator('.cm-content')).not.toHaveText(originalCode) | ||||
|  | ||||
|       // Test that the code is not empty when you click on the next step | ||||
|       await page.locator('[data-testid="onboarding-next"]').hover() | ||||
|       await page.locator('[data-testid="onboarding-next"]').click() | ||||
|       await expect(page.locator('.cm-content')).toHaveText(/.+/) | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     'Onboarding code gets reset to demo on Interactive Numbers step', | ||||
|     { | ||||
|       appSettings: { | ||||
|         app: { | ||||
|           onboardingStatus: '/parametric-modeling', | ||||
|         }, | ||||
|       }, | ||||
|       cleanProjectDir: true, | ||||
|     }, | ||||
|  | ||||
|     async ({ context, page, homePage }) => { | ||||
|       const u = await getUtils(page) | ||||
|       const badCode = `// This is bad code we shouldn't see` | ||||
|  | ||||
|       await page.setBodyDimensions({ width: 1200, height: 1080 }) | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|       await expect | ||||
|         .poll(() => page.url()) | ||||
|         .toContain(onboardingPaths.PARAMETRIC_MODELING) | ||||
|  | ||||
|       const bracketNoNewLines = bracket.replace(/\n/g, '') | ||||
|  | ||||
|       // Check the code got reset on load | ||||
|       await expect(page.locator('#code-pane')).toBeVisible() | ||||
|       await expect(u.codeLocator).toHaveText(bracketNoNewLines, { | ||||
|         timeout: 10_000, | ||||
|       }) | ||||
|  | ||||
|       // Mess with the code again | ||||
|       await u.codeLocator.selectText() | ||||
|       await u.codeLocator.fill(badCode) | ||||
|       await expect(u.codeLocator).toHaveText(badCode) | ||||
|  | ||||
|       // Click to the next step | ||||
|       await page.locator('[data-testid="onboarding-next"]').hover() | ||||
|       await page.locator('[data-testid="onboarding-next"]').click() | ||||
|       await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, { | ||||
|         waitUntil: 'domcontentloaded', | ||||
|       }) | ||||
|  | ||||
|       // Check that the code has been reset | ||||
|       await expect(u.codeLocator).toHaveText(bracketNoNewLines) | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   // (lee) The two avatar tests are weird because even on main, we don't have | ||||
|   // anything to do with the avatar inside the onboarding test. Due to the | ||||
|   // low impact of an avatar not showing I'm changing this to fixme. | ||||
|   test.fixme( | ||||
|     'Avatar text updates depending on image load success', | ||||
|     { | ||||
|       appSettings: { | ||||
|         app: { | ||||
|           onboardingStatus: 'incomplete', | ||||
|         }, | ||||
|       }, | ||||
|       cleanProjectDir: true, | ||||
|     }, | ||||
|     async ({ context, page, homePage }) => { | ||||
|       // Override beforeEach test setup | ||||
|       await context.addInitScript( | ||||
|         async ({ settingsKey, settings }) => { | ||||
|           localStorage.setItem(settingsKey, settings) | ||||
|         }, | ||||
|         { | ||||
|           settingsKey: TEST_SETTINGS_KEY, | ||||
|           settings: TOML.stringify({ | ||||
|             settings: TEST_SETTINGS_ONBOARDING_USER_MENU, | ||||
|           }), | ||||
|         } | ||||
|       ) | ||||
|  | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|       // Test that the text in this step is correct | ||||
|       const avatarLocator = await page | ||||
|         .getByTestId('user-sidebar-toggle') | ||||
|         .locator('img') | ||||
|       const onboardingOverlayLocator = await page | ||||
|         .getByTestId('onboarding-content') | ||||
|         .locator('div') | ||||
|         .nth(1) | ||||
|  | ||||
|       // Expect the avatar to be visible and for the text to reference it | ||||
|       await expect(avatarLocator).toBeVisible() | ||||
|       await expect(onboardingOverlayLocator).toBeVisible() | ||||
|       await expect(onboardingOverlayLocator).toContainText('your avatar') | ||||
|  | ||||
|       // This is to force the avatar to 404. | ||||
|       // For our test image (only triggers locally. on CI, it's Kurt's / | ||||
|       // gravatar image ) | ||||
|       await page.route('/cat.jpg', async (route) => { | ||||
|         await route.fulfill({ | ||||
|           status: 404, | ||||
|           contentType: 'text/plain', | ||||
|           body: 'Not Found!', | ||||
|         }) | ||||
|       }) | ||||
|  | ||||
|       // 404 the CI avatar image | ||||
|       await page.route( | ||||
|         'https://lh3.googleusercontent.com/**', | ||||
|         async (route) => { | ||||
|           await route.fulfill({ | ||||
|             status: 404, | ||||
|             contentType: 'text/plain', | ||||
|             body: 'Not Found!', | ||||
|           }) | ||||
|         } | ||||
|       ) | ||||
|  | ||||
|       await page.reload({ waitUntil: 'domcontentloaded' }) | ||||
|  | ||||
|       // Now expect the text to be different | ||||
|       await expect(avatarLocator).not.toBeVisible() | ||||
|       await expect(onboardingOverlayLocator).toBeVisible() | ||||
|       await expect(onboardingOverlayLocator).toContainText('the menu button') | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test.fixme( | ||||
|     "Avatar text doesn't mention avatar when no avatar", | ||||
|     { | ||||
|       appSettings: { | ||||
|         app: { | ||||
|           onboardingStatus: 'incomplete', | ||||
|         }, | ||||
|       }, | ||||
|       cleanProjectDir: true, | ||||
|     }, | ||||
|     async ({ context, page, homePage }) => { | ||||
|       // Override beforeEach test setup | ||||
|       await context.addInitScript( | ||||
|         async ({ settingsKey, settings }) => { | ||||
|           localStorage.setItem(settingsKey, settings) | ||||
|           localStorage.setItem('FORCE_NO_IMAGE', 'FORCE_NO_IMAGE') | ||||
|         }, | ||||
|         { | ||||
|           settingsKey: TEST_SETTINGS_KEY, | ||||
|           settings: TOML.stringify({ | ||||
|             settings: TEST_SETTINGS_ONBOARDING_USER_MENU, | ||||
|           }), | ||||
|         } | ||||
|       ) | ||||
|  | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|       await homePage.goToModelingScene() | ||||
|  | ||||
|       // Test that the text in this step is correct | ||||
|       const sidebar = page.getByTestId('user-sidebar-toggle') | ||||
|       const avatar = sidebar.locator('img') | ||||
|       const onboardingOverlayLocator = page | ||||
|         .getByTestId('onboarding-content') | ||||
|         .locator('div') | ||||
|         .nth(1) | ||||
|  | ||||
|       // Expect the avatar to be visible and for the text to reference it | ||||
|       await expect(avatar).not.toBeVisible() | ||||
|       await expect(onboardingOverlayLocator).toBeVisible() | ||||
|       await expect(onboardingOverlayLocator).toContainText('the menu button') | ||||
|  | ||||
|       // Test we mention what else is in this menu for https://github.com/KittyCAD/modeling-app/issues/2939 | ||||
|       // which doesn't deserver its own full test spun up | ||||
|       const userMenuFeatures = [ | ||||
|         'manage your account', | ||||
|         'report a bug', | ||||
|         'request a feature', | ||||
|         'sign out', | ||||
|       ] | ||||
|       for (const feature of userMenuFeatures) { | ||||
|         await expect(onboardingOverlayLocator).toContainText(feature) | ||||
|       } | ||||
|     } | ||||
|   ) | ||||
| }) | ||||
|  | ||||
| test( | ||||
| test.fixme( | ||||
|   'Restarting onboarding on desktop takes one attempt', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browser: _ }, testInfo) => { | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         const routerTemplateDir = join(dir, 'router-template-slate') | ||||
|         await fsp.mkdir(routerTemplateDir, { recursive: true }) | ||||
|         await fsp.copyFile( | ||||
|           executorInputPath('router-template-slate.kcl'), | ||||
|           join(routerTemplateDir, 'main.kcl') | ||||
|         ) | ||||
|   { | ||||
|     appSettings: { | ||||
|       app: { | ||||
|         onboardingStatus: 'dismissed', | ||||
|       }, | ||||
|     }, | ||||
|     cleanProjectDir: true, | ||||
|   }, | ||||
|   async ({ context, page, homePage }, testInfo) => { | ||||
|     await context.folderSetupFn(async (dir) => { | ||||
|       const routerTemplateDir = join(dir, 'router-template-slate') | ||||
|       await fsp.mkdir(routerTemplateDir, { recursive: true }) | ||||
|       await fsp.copyFile( | ||||
|         executorInputPath('router-template-slate.kcl'), | ||||
|         join(routerTemplateDir, 'main.kcl') | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     // Our constants | ||||
| @ -418,9 +466,8 @@ test( | ||||
|     const restartOnboardingButton = page.getByRole('button', { | ||||
|       name: 'Reset onboarding', | ||||
|     }) | ||||
|     const restartConfirmationButton = page.getByRole('button', { | ||||
|       name: 'Make a new project', | ||||
|     }) | ||||
|     const nextButton = page.getByTestId('onboarding-next') | ||||
|  | ||||
|     const tutorialProjectIndicator = page | ||||
|       .getByTestId('project-sidebar-toggle') | ||||
|       .filter({ hasText: 'Tutorial Project 00' }) | ||||
| @ -439,7 +486,7 @@ test( | ||||
|     }) | ||||
|  | ||||
|     await test.step('Navigate into project', async () => { | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
|       page.on('console', console.log) | ||||
|  | ||||
| @ -455,14 +502,22 @@ test( | ||||
|       await helpMenuButton.click() | ||||
|       await restartOnboardingButton.click() | ||||
|  | ||||
|       await expect(restartConfirmationButton).toBeVisible() | ||||
|       await restartConfirmationButton.click() | ||||
|       await nextButton.hover() | ||||
|       await nextButton.click() | ||||
|     }) | ||||
|  | ||||
|     await test.step('Confirm that the onboarding has restarted', async () => { | ||||
|       await expect(tutorialProjectIndicator).toBeVisible() | ||||
|       await expect(tutorialModalText).toBeVisible() | ||||
|       // Make sure the model loaded | ||||
|       const XYPlanePoint = { x: 988, y: 523 } as const | ||||
|       const modelColor: [number, number, number] = [76, 76, 76] | ||||
|  | ||||
|       await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) | ||||
|       await expectPixelColor(page, modelColor, XYPlanePoint, 8) | ||||
|       await tutorialDismissButton.click() | ||||
|       // Make sure model still there. | ||||
|       await expectPixelColor(page, modelColor, XYPlanePoint, 8) | ||||
|     }) | ||||
|  | ||||
|     await test.step('Clear code and restart onboarding from settings', async () => { | ||||
| @ -480,11 +535,9 @@ test( | ||||
|  | ||||
|       await restartOnboardingSettingsButton.click() | ||||
|       // Since the code is empty, we should not see the confirmation dialog | ||||
|       await expect(restartConfirmationButton).not.toBeVisible() | ||||
|       await expect(nextButton).not.toBeVisible() | ||||
|       await expect(tutorialProjectIndicator).toBeVisible() | ||||
|       await expect(tutorialModalText).toBeVisible() | ||||
|     }) | ||||
|  | ||||
|     await electronApp.close() | ||||
|   } | ||||
| ) | ||||
|  | ||||
| @ -1,25 +1,51 @@ | ||||
| import { test, expect, AuthenticatedApp } from './fixtures/fixtureSetup' | ||||
| import { test, expect, Page } from './zoo-test' | ||||
| import { EditorFixture } from './fixtures/editorFixture' | ||||
| import { SceneFixture } from './fixtures/sceneFixture' | ||||
| import { ToolbarFixture } from './fixtures/toolbarFixture' | ||||
| import fs from 'node:fs/promises' | ||||
| import path from 'node:path' | ||||
| import { getUtils } from './test-utils' | ||||
|  | ||||
| // test file is for testing point an click code gen functionality that's not sketch mode related | ||||
|  | ||||
| test( | ||||
|   'verify extruding circle works', | ||||
|   { tag: ['@skipWin'] }, | ||||
|   async ({ app, cmdBar, editor, toolbar, scene }) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'Fails on windows in CI, can not be replicated locally on windows.' | ||||
|     ) | ||||
|     const file = await app.getInputFile('test-circle-extrude.kcl') | ||||
|     await app.initialise(file) | ||||
|     const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217) | ||||
| test('verify extruding circle works', async ({ | ||||
|   context, | ||||
|   homePage, | ||||
|   cmdBar, | ||||
|   editor, | ||||
|   toolbar, | ||||
|   scene, | ||||
| }) => { | ||||
|   // TODO: fix this test on windows after the electron migration | ||||
|   test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|   const file = await fs.readFile( | ||||
|     path.resolve( | ||||
|       __dirname, | ||||
|       '../../', | ||||
|       './src/wasm-lib/tests/executor/inputs/test-circle-extrude.kcl' | ||||
|     ), | ||||
|     'utf-8' | ||||
|   ) | ||||
|   await context.addInitScript((file) => { | ||||
|     localStorage.setItem('persistCode', file) | ||||
|   }, file) | ||||
|   await homePage.goToModelingScene() | ||||
|  | ||||
|     await test.step('because there is sweepable geometry, verify extrude is enable when nothing is selected', async () => { | ||||
|       await scene.clickNoWhere() | ||||
|       await expect(toolbar.extrudeButton).toBeEnabled() | ||||
|   const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217) | ||||
|  | ||||
|   await test.step('because there is sweepable geometry, verify extrude is enable when nothing is selected', async () => { | ||||
|     await scene.clickNoWhere() | ||||
|     await expect(toolbar.extrudeButton).toBeEnabled() | ||||
|   }) | ||||
|  | ||||
|   await test.step('check code model connection works and that button is still enable once circle is selected ', async () => { | ||||
|     await moveToCircle() | ||||
|     const circleSnippet = | ||||
|       'circle({ center = [318.33, 168.1], radius = 182.8 }, %)' | ||||
|     await editor.expectState({ | ||||
|       activeLines: ["constsketch002=startSketchOn('XZ')"], | ||||
|       highlightedCode: circleSnippet, | ||||
|       diagnostics: [], | ||||
|     }) | ||||
|  | ||||
|     await test.step('check code model connection works and that button is still enable once circle is selected ', async () => { | ||||
| @ -27,7 +53,7 @@ test( | ||||
|       const circleSnippet = | ||||
|         'circle({ center = [318.33, 168.1], radius = 182.8 }, %)' | ||||
|       await editor.expectState({ | ||||
|         activeLines: [], | ||||
|         activeLines: ["constsketch002=startSketchOn('XZ')"], | ||||
|         highlightedCode: circleSnippet, | ||||
|         diagnostics: [], | ||||
|       }) | ||||
| @ -40,39 +66,42 @@ test( | ||||
|       }) | ||||
|       await expect(toolbar.extrudeButton).toBeEnabled() | ||||
|     }) | ||||
|     await expect(toolbar.extrudeButton).toBeEnabled() | ||||
|   }) | ||||
|  | ||||
|     await test.step('do extrude flow and check extrude code is added to editor', async () => { | ||||
|       await toolbar.extrudeButton.click() | ||||
|   await test.step('do extrude flow and check extrude code is added to editor', async () => { | ||||
|     await toolbar.extrudeButton.click() | ||||
|  | ||||
|       await cmdBar.expectState({ | ||||
|         stage: 'arguments', | ||||
|         currentArgKey: 'distance', | ||||
|         currentArgValue: '5', | ||||
|         headerArguments: { Selection: '1 face', Distance: '' }, | ||||
|         highlightedHeaderArg: 'distance', | ||||
|         commandName: 'Extrude', | ||||
|       }) | ||||
|       await cmdBar.progressCmdBar() | ||||
|  | ||||
|       const expectString = 'extrude001 = extrude(5, sketch001)' | ||||
|       await editor.expectEditor.not.toContain(expectString) | ||||
|  | ||||
|       await cmdBar.expectState({ | ||||
|         stage: 'review', | ||||
|         headerArguments: { Selection: '1 face', Distance: '5' }, | ||||
|         commandName: 'Extrude', | ||||
|       }) | ||||
|       await cmdBar.progressCmdBar() | ||||
|  | ||||
|       await editor.expectEditor.toContain(expectString) | ||||
|     await cmdBar.expectState({ | ||||
|       stage: 'arguments', | ||||
|       currentArgKey: 'distance', | ||||
|       currentArgValue: '5', | ||||
|       headerArguments: { Selection: '1 face', Distance: '' }, | ||||
|       highlightedHeaderArg: 'distance', | ||||
|       commandName: 'Extrude', | ||||
|     }) | ||||
|   } | ||||
| ) | ||||
|     await cmdBar.progressCmdBar() | ||||
|  | ||||
|     const expectString = 'extrude001 = extrude(5, sketch001)' | ||||
|     await editor.expectEditor.not.toContain(expectString) | ||||
|  | ||||
|     await cmdBar.expectState({ | ||||
|       stage: 'review', | ||||
|       headerArguments: { Selection: '1 face', Distance: '5' }, | ||||
|       commandName: 'Extrude', | ||||
|     }) | ||||
|     await cmdBar.progressCmdBar() | ||||
|  | ||||
|     await editor.expectEditor.toContain(expectString) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test.describe('verify sketch on chamfer works', () => { | ||||
|   // TODO: fix this test on windows after the electron migration | ||||
|   test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|   const _sketchOnAChamfer = | ||||
|     ( | ||||
|       app: AuthenticatedApp, | ||||
|       page: Page, | ||||
|       editor: EditorFixture, | ||||
|       toolbar: ToolbarFixture, | ||||
|       scene: SceneFixture | ||||
| @ -124,7 +153,7 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|         await toolbar.startSketchPlaneSelection() | ||||
|         await clickChamfer() | ||||
|         // timeout wait for engine animation is unavoidable | ||||
|         await app.page.waitForTimeout(600) | ||||
|         await page.waitForTimeout(1000) | ||||
|         await editor.expectEditor.toContain(afterChamferSelectSnippet) | ||||
|       }) | ||||
|       await test.step('make sure a basic sketch can be added', async () => { | ||||
| @ -135,7 +164,9 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|           pixelDiff: 50, | ||||
|         }) | ||||
|         await rectangle2ndClick() | ||||
|         await editor.expectEditor.toContain(afterRectangle2ndClickSnippet) | ||||
|         await editor.expectEditor.toContain(afterRectangle2ndClickSnippet, { | ||||
|           shouldNormalise: true, | ||||
|         }) | ||||
|       }) | ||||
|  | ||||
|       await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => { | ||||
| @ -150,24 +181,35 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|         }) | ||||
|       }) | ||||
|     } | ||||
|   test( | ||||
|     'works on all edge selections and can break up multi edges in a chamfer array', | ||||
|     { tag: ['@skipWin'] }, | ||||
|     async ({ app, editor, toolbar, scene }) => { | ||||
|       test.skip( | ||||
|         process.platform === 'win32', | ||||
|         'Fails on windows in CI, can not be replicated locally on windows.' | ||||
|       ) | ||||
|       const file = await app.getInputFile('e2e-can-sketch-on-chamfer.kcl') | ||||
|       await app.initialise(file) | ||||
|   test('works on all edge selections and can break up multi edges in a chamfer array', async ({ | ||||
|     context, | ||||
|     page, | ||||
|     homePage, | ||||
|     editor, | ||||
|     toolbar, | ||||
|     scene, | ||||
|   }) => { | ||||
|     const file = await fs.readFile( | ||||
|       path.resolve( | ||||
|         __dirname, | ||||
|         '../../', | ||||
|         './src/wasm-lib/tests/executor/inputs/e2e-can-sketch-on-chamfer.kcl' | ||||
|       ), | ||||
|       'utf-8' | ||||
|     ) | ||||
|     await context.addInitScript((file) => { | ||||
|       localStorage.setItem('persistCode', file) | ||||
|     }, file) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|       const sketchOnAChamfer = _sketchOnAChamfer(app, editor, toolbar, scene) | ||||
|     const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene) | ||||
|  | ||||
|       await sketchOnAChamfer({ | ||||
|         clickCoords: { x: 570, y: 220 }, | ||||
|         cameraPos: { x: 16020, y: -2000, z: 10500 }, | ||||
|         cameraTarget: { x: -150, y: -4500, z: -80 }, | ||||
|         beforeChamferSnippet: `angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01) | ||||
|     await sketchOnAChamfer({ | ||||
|       clickCoords: { x: 570, y: 220 }, | ||||
|       cameraPos: { x: 16020, y: -2000, z: 10500 }, | ||||
|       cameraTarget: { x: -150, y: -4500, z: -80 }, | ||||
|       beforeChamferSnippet: `angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01) | ||||
|       chamfer({length = 30,tags = [ | ||||
|       seg01, | ||||
|       getNextAdjacentEdge(yo), | ||||
| @ -175,10 +217,9 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|       getOppositeEdge(seg01) | ||||
|     ]}, %)`, | ||||
|  | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch002 = startSketchOn(extrude001, seg03)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) | ||||
|       afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)', | ||||
|       afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', | ||||
|       afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA002) - 90, | ||||
|          105.26 | ||||
| @ -189,13 +230,13 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|        ], %, $rectangleSegmentC001) | ||||
|     |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|     |> close(%)`, | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|       await sketchOnAChamfer({ | ||||
|         clickCoords: { x: 690, y: 250 }, | ||||
|         cameraPos: { x: 16020, y: -2000, z: 10500 }, | ||||
|         cameraTarget: { x: -150, y: -4500, z: -80 }, | ||||
|         beforeChamferSnippet: `angledLine([ | ||||
|     await sketchOnAChamfer({ | ||||
|       clickCoords: { x: 690, y: 250 }, | ||||
|       cameraPos: { x: 16020, y: -2000, z: 10500 }, | ||||
|       cameraTarget: { x: -150, y: -4500, z: -80 }, | ||||
|       beforeChamferSnippet: `angledLine([ | ||||
|          segAng(rectangleSegmentA001) - 90, | ||||
|          217.26 | ||||
|        ], %, $seg01)chamfer({ | ||||
| @ -207,10 +248,9 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|          ] | ||||
|        }, %)`, | ||||
|  | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch003 = startSketchOn(extrude001, seg04)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) | ||||
|       afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)', | ||||
|       afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', | ||||
|       afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA003) - 90, | ||||
|          106.84 | ||||
| @ -221,22 +261,21 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|        ], %, $rectangleSegmentC002) | ||||
|     |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|     |> close(%)`, | ||||
|       }) | ||||
|       await sketchOnAChamfer({ | ||||
|         clickCoords: { x: 677, y: 87 }, | ||||
|         cameraPos: { x: -6200, y: 1500, z: 6200 }, | ||||
|         cameraTarget: { x: 8300, y: 1100, z: 4800 }, | ||||
|         beforeChamferSnippet: `angledLine([0, 268.43], %, $rectangleSegmentA001)chamfer({ | ||||
|     }) | ||||
|     await sketchOnAChamfer({ | ||||
|       clickCoords: { x: 677, y: 87 }, | ||||
|       cameraPos: { x: -6200, y: 1500, z: 6200 }, | ||||
|       cameraTarget: { x: 8300, y: 1100, z: 4800 }, | ||||
|       beforeChamferSnippet: `angledLine([0, 268.43], %, $rectangleSegmentA001)chamfer({ | ||||
|          length = 30, | ||||
|          tags = [ | ||||
|            getNextAdjacentEdge(yo), | ||||
|            getNextAdjacentEdge(seg02) | ||||
|          ] | ||||
|        }, %)`, | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch003 = startSketchOn(extrude001, seg04)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) | ||||
|       afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)', | ||||
|       afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)', | ||||
|       afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA003) - 90, | ||||
|          106.84 | ||||
| @ -247,20 +286,19 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|        ], %, $rectangleSegmentC002) | ||||
|     |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|     |> close(%)`, | ||||
|       }) | ||||
|       /// last one | ||||
|       await sketchOnAChamfer({ | ||||
|         clickCoords: { x: 620, y: 300 }, | ||||
|         cameraPos: { x: -1100, y: -7700, z: 1600 }, | ||||
|         cameraTarget: { x: 1450, y: 670, z: 4000 }, | ||||
|         beforeChamferSnippet: `chamfer({ | ||||
|     }) | ||||
|     /// last one | ||||
|     await sketchOnAChamfer({ | ||||
|       clickCoords: { x: 620, y: 300 }, | ||||
|       cameraPos: { x: -1100, y: -7700, z: 1600 }, | ||||
|       cameraTarget: { x: 1450, y: 670, z: 4000 }, | ||||
|       beforeChamferSnippet: `chamfer({ | ||||
|          length = 30, | ||||
|          tags = [getNextAdjacentEdge(yo)] | ||||
|        }, %)`, | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch005 = startSketchOn(extrude001, seg06)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005) | ||||
|       afterChamferSelectSnippet: 'sketch005 = startSketchOn(extrude001, seg06)', | ||||
|       afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)', | ||||
|       afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005) | ||||
|  | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA005) - 90, | ||||
| @ -272,11 +310,11 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|        ], %, $rectangleSegmentC004) | ||||
|     |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|     |> close(%)`, | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|       await test.step('verify at the end of the test that final code is what is expected', async () => { | ||||
|         await editor.expectEditor.toContain( | ||||
|           `sketch001 = startSketchOn('XZ') | ||||
|     await test.step('verify at the end of the test that final code is what is expected', async () => { | ||||
|       await editor.expectEditor.toContain( | ||||
|         `sketch001 = startSketchOn('XZ') | ||||
|  | ||||
|       |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag] | ||||
|       |> angledLine([0, 268.43], %, $rectangleSegmentA001) | ||||
| @ -305,7 +343,7 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|            tags = [getNextAdjacentEdge(yo)] | ||||
|          }, %, $seg06) | ||||
|     sketch005 = startSketchOn(extrude001, seg06) | ||||
|       |> startProfileAt([-23.43, 19.69], %) | ||||
|       |> startProfileAt([-23.43,19.69], %) | ||||
|       |> angledLine([0, 9.1], %, $rectangleSegmentA005) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA005) - 90, | ||||
| @ -318,7 +356,7 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|       |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|       |> close(%) | ||||
|     sketch004 = startSketchOn(extrude001, seg05) | ||||
|       |> startProfileAt([82.57, 322.96], %) | ||||
|       |> startProfileAt([82.57,322.96], %) | ||||
|       |> angledLine([0, 11.16], %, $rectangleSegmentA004) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA004) - 90, | ||||
| @ -331,7 +369,7 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|       |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|       |> close(%) | ||||
|     sketch003 = startSketchOn(extrude001, seg04) | ||||
|       |> startProfileAt([-209.64, 255.28], %) | ||||
|       |> startProfileAt([-209.64,255.28], %) | ||||
|       |> angledLine([0, 11.56], %, $rectangleSegmentA003) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA003) - 90, | ||||
| @ -344,7 +382,7 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|       |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|       |> close(%) | ||||
|     sketch002 = startSketchOn(extrude001, seg03) | ||||
|       |> startProfileAt([205.96, 254.59], %) | ||||
|       |> startProfileAt([205.96,254.59], %) | ||||
|       |> angledLine([0, 11.39], %, $rectangleSegmentA002) | ||||
|       |> angledLine([ | ||||
|            segAng(rectangleSegmentA002) - 90, | ||||
| @ -357,43 +395,50 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|       |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|       |> close(%) | ||||
|     `, | ||||
|           { shouldNormalise: true } | ||||
|         ) | ||||
|       }) | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     'Works on chamfers that are non in a pipeExpression can break up multi edges in a chamfer array', | ||||
|     { tag: ['@skipWin'] }, | ||||
|     async ({ app, editor, toolbar, scene }) => { | ||||
|       test.skip( | ||||
|         process.platform === 'win32', | ||||
|         'Fails on windows in CI, can not be replicated locally on windows.' | ||||
|         { shouldNormalise: true } | ||||
|       ) | ||||
|       const file = await app.getInputFile( | ||||
|         'e2e-can-sketch-on-chamfer-no-pipeExpr.kcl' | ||||
|       ) | ||||
|       await app.initialise(file) | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|       const sketchOnAChamfer = _sketchOnAChamfer(app, editor, toolbar, scene) | ||||
|   test('Works on chamfers that are non in a pipeExpression can break up multi edges in a chamfer array', async ({ | ||||
|     context, | ||||
|     page, | ||||
|     homePage, | ||||
|     editor, | ||||
|     toolbar, | ||||
|     scene, | ||||
|   }) => { | ||||
|     const file = await fs.readFile( | ||||
|       path.resolve( | ||||
|         __dirname, | ||||
|         '../../', | ||||
|         './src/wasm-lib/tests/executor/inputs/e2e-can-sketch-on-chamfer-no-pipeExpr.kcl' | ||||
|       ), | ||||
|       'utf-8' | ||||
|     ) | ||||
|     await context.addInitScript((file) => { | ||||
|       localStorage.setItem('persistCode', file) | ||||
|     }, file) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|       await sketchOnAChamfer({ | ||||
|         clickCoords: { x: 570, y: 220 }, | ||||
|         cameraPos: { x: 16020, y: -2000, z: 10500 }, | ||||
|         cameraTarget: { x: -150, y: -4500, z: -80 }, | ||||
|         beforeChamferSnippet: `angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01) | ||||
|     const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene) | ||||
|  | ||||
|     await sketchOnAChamfer({ | ||||
|       clickCoords: { x: 570, y: 220 }, | ||||
|       cameraPos: { x: 16020, y: -2000, z: 10500 }, | ||||
|       cameraTarget: { x: -150, y: -4500, z: -80 }, | ||||
|       beforeChamferSnippet: `angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01) | ||||
|       chamfer({length=30,tags=[ | ||||
|       seg01, | ||||
|       getNextAdjacentEdge(yo), | ||||
|       getNextAdjacentEdge(seg02), | ||||
|       getOppositeEdge(seg01) | ||||
|     ]}, extrude001)`, | ||||
|         beforeChamferSnippetEnd: '}, extrude001)', | ||||
|         afterChamferSelectSnippet: | ||||
|           'sketch002 = startSketchOn(extrude001, seg03)', | ||||
|         afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', | ||||
|         afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) | ||||
|       beforeChamferSnippetEnd: '}, extrude001)', | ||||
|       afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)', | ||||
|       afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', | ||||
|       afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) | ||||
|     |> angledLine([ | ||||
|          segAng(rectangleSegmentA002) - 90, | ||||
|          105.26 | ||||
| @ -404,9 +449,9 @@ test.describe('verify sketch on chamfer works', () => { | ||||
|        ], %, $rectangleSegmentC001) | ||||
|     |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|     |> close(%)`, | ||||
|       }) | ||||
|       await editor.expectEditor.toContain( | ||||
|         `sketch001 = startSketchOn('XZ') | ||||
|     }) | ||||
|     await editor.expectEditor.toContain( | ||||
|       `sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([75.8, 317.2], %) | ||||
|   |> angledLine([0, 268.43], %, $rectangleSegmentA001) | ||||
|   |> angledLine([ | ||||
| @ -446,50 +491,56 @@ sketch002 = startSketchOn(extrude001, seg03) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| `, | ||||
|         { shouldNormalise: true } | ||||
|       ) | ||||
|     } | ||||
|   ) | ||||
|       { shouldNormalise: true } | ||||
|     ) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test(`Verify axis, origin, and horizontal snapping`, async ({ | ||||
|   app, | ||||
|   page, | ||||
|   homePage, | ||||
|   editor, | ||||
|   toolbar, | ||||
|   scene, | ||||
| }) => { | ||||
|   const viewPortSize = { width: 1200, height: 500 } | ||||
|  | ||||
|   await page.setBodyDimensions(viewPortSize) | ||||
|  | ||||
|   await homePage.goToModelingScene() | ||||
|  | ||||
|   // Constants and locators | ||||
|   // These are mappings from screenspace to KCL coordinates, | ||||
|   // until we merge in our coordinate system helpers | ||||
|   const xzPlane = [ | ||||
|     app.viewPortSize.width * 0.65, | ||||
|     app.viewPortSize.height * 0.3, | ||||
|     viewPortSize.width * 0.65, | ||||
|     viewPortSize.height * 0.3, | ||||
|   ] as const | ||||
|   const originSloppy = { | ||||
|     screen: [ | ||||
|       app.viewPortSize.width / 2 + 3, // 3px off the center of the screen | ||||
|       app.viewPortSize.height / 2, | ||||
|       viewPortSize.width / 2 + 3, // 3px off the center of the screen | ||||
|       viewPortSize.height / 2, | ||||
|     ], | ||||
|     kcl: [0, 0], | ||||
|   } as const | ||||
|   const xAxisSloppy = { | ||||
|     screen: [ | ||||
|       app.viewPortSize.width * 0.75, | ||||
|       app.viewPortSize.height / 2 - 3, // 3px off the X-axis | ||||
|       viewPortSize.width * 0.75, | ||||
|       viewPortSize.height / 2 - 3, // 3px off the X-axis | ||||
|     ], | ||||
|     kcl: [16.95, 0], | ||||
|     kcl: [20.34, 0], | ||||
|   } as const | ||||
|   const offYAxis = { | ||||
|     screen: [ | ||||
|       app.viewPortSize.width * 0.6, // Well off the Y-axis, out of snapping range | ||||
|       app.viewPortSize.height * 0.3, | ||||
|       viewPortSize.width * 0.6, // Well off the Y-axis, out of snapping range | ||||
|       viewPortSize.height * 0.3, | ||||
|     ], | ||||
|     kcl: [6.78, 6.78], | ||||
|     kcl: [8.14, 6.78], | ||||
|   } as const | ||||
|   const yAxisSloppy = { | ||||
|     screen: [ | ||||
|       app.viewPortSize.width / 2 + 5, // 5px off the Y-axis | ||||
|       app.viewPortSize.height * 0.3, | ||||
|       viewPortSize.width / 2 + 5, // 5px off the Y-axis | ||||
|       viewPortSize.height * 0.3, | ||||
|     ], | ||||
|     kcl: [0, 6.78], | ||||
|   } as const | ||||
| @ -510,15 +561,13 @@ test(`Verify axis, origin, and horizontal snapping`, async ({ | ||||
|     afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`, | ||||
|   } | ||||
|  | ||||
|   await app.initialise() | ||||
|  | ||||
|   await test.step(`Start a sketch on the XZ plane`, async () => { | ||||
|     await editor.closePane() | ||||
|     await toolbar.startSketchPlaneSelection() | ||||
|     await moveToXzPlane() | ||||
|     await clickOnXzPlane() | ||||
|     // timeout wait for engine animation is unavoidable | ||||
|     await app.page.waitForTimeout(600) | ||||
|     await page.waitForTimeout(600) | ||||
|     await editor.expectEditor.toContain(expectedCodeSnippets.sketchOnXzPlane) | ||||
|   }) | ||||
|   await test.step(`Place a point a few pixels off the middle, verify it still snaps to 0,0`, async () => { | ||||
| @ -553,11 +602,15 @@ test(`Verify axis, origin, and horizontal snapping`, async ({ | ||||
| }) | ||||
|  | ||||
| test(`Verify user can double-click to edit a sketch`, async ({ | ||||
|   app, | ||||
|   context, | ||||
|   page, | ||||
|   homePage, | ||||
|   editor, | ||||
|   toolbar, | ||||
|   scene, | ||||
| }) => { | ||||
|   const u = await getUtils(page) | ||||
|  | ||||
|   const initialCode = `closedSketch = startSketchOn('XZ') | ||||
|   |> circle({ center = [8, 5], radius = 2 }, %) | ||||
| openSketch = startSketchOn('XY') | ||||
| @ -566,15 +619,24 @@ openSketch = startSketchOn('XY') | ||||
|   |> xLine(5, %) | ||||
|   |> tangentialArcTo([10, 0], %) | ||||
| ` | ||||
|   await app.initialise(initialCode) | ||||
|   const viewPortSize = { width: 1000, height: 500 } | ||||
|   await page.setBodyDimensions(viewPortSize) | ||||
|  | ||||
|   await context.addInitScript((code) => { | ||||
|     localStorage.setItem('persistCode', code) | ||||
|   }, initialCode) | ||||
|  | ||||
|   await homePage.goToModelingScene() | ||||
|   await u.waitForPageLoad() | ||||
|   await page.waitForTimeout(1000) | ||||
|  | ||||
|   const pointInsideCircle = { | ||||
|     x: app.viewPortSize.width * 0.63, | ||||
|     y: app.viewPortSize.height * 0.5, | ||||
|     x: viewPortSize.width * 0.63, | ||||
|     y: viewPortSize.height * 0.5, | ||||
|   } | ||||
|   const pointOnPathAfterSketching = { | ||||
|     x: app.viewPortSize.width * 0.58, | ||||
|     y: app.viewPortSize.height * 0.5, | ||||
|     x: viewPortSize.width * 0.65, | ||||
|     y: viewPortSize.height * 0.5, | ||||
|   } | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   const [_clickOpenPath, moveToOpenPath, dblClickOpenPath] = | ||||
| @ -607,41 +669,59 @@ openSketch = startSketchOn('XY') | ||||
|       diagnostics: [], | ||||
|     }) | ||||
|   }) | ||||
|   await page.waitForTimeout(1000) | ||||
|  | ||||
|   await exitSketch() | ||||
|   await page.waitForTimeout(1000) | ||||
|  | ||||
|   // Drag the sketch line out of the axis view which blocks the click | ||||
|   await page.dragAndDrop('#stream', '#stream', { | ||||
|     sourcePosition: { | ||||
|       x: viewPortSize.width * 0.7, | ||||
|       y: viewPortSize.height * 0.5, | ||||
|     }, | ||||
|     targetPosition: { | ||||
|       x: viewPortSize.width * 0.7, | ||||
|       y: viewPortSize.height * 0.4, | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   await page.waitForTimeout(500) | ||||
|  | ||||
|   await test.step(`Double-click on the open sketch`, async () => { | ||||
|     await moveToOpenPath() | ||||
|     await scene.expectPixelColor([250, 250, 250], pointOnPathAfterSketching, 15) | ||||
|     // There is a full execution after exiting sketch that clears the scene. | ||||
|     await app.page.waitForTimeout(500) | ||||
|     await page.waitForTimeout(500) | ||||
|     await dblClickOpenPath() | ||||
|     await expect(toolbar.startSketchBtn).not.toBeVisible() | ||||
|     await expect(toolbar.exitSketchBtn).toBeVisible() | ||||
|     // Wait for enter sketch mode to complete | ||||
|     await app.page.waitForTimeout(500) | ||||
|     await page.waitForTimeout(500) | ||||
|     await editor.expectState({ | ||||
|       activeLines: [`|>xLine(5,%)`], | ||||
|       highlightedCode: 'xLine(5,%)', | ||||
|       activeLines: [`|>tangentialArcTo([10,0],%)`], | ||||
|       highlightedCode: 'tangentialArcTo([10,0],%)', | ||||
|       diagnostics: [], | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test(`Offset plane point-and-click`, async ({ | ||||
|   app, | ||||
|   context, | ||||
|   page, | ||||
|   homePage, | ||||
|   scene, | ||||
|   editor, | ||||
|   toolbar, | ||||
|   cmdBar, | ||||
| }) => { | ||||
|   await app.initialise() | ||||
|  | ||||
|   // One dumb hardcoded screen pixel value | ||||
|   const testPoint = { x: 700, y: 150 } | ||||
|   const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y) | ||||
|   const expectedOutput = `plane001 = offsetPlane('XZ', 5)` | ||||
|  | ||||
|   await homePage.goToModelingScene() | ||||
|  | ||||
|   await test.step(`Look for the blue of the XZ plane`, async () => { | ||||
|     await scene.expectPixelColor([50, 51, 96], testPoint, 15) | ||||
|   }) | ||||
| @ -677,3 +757,277 @@ test(`Offset plane point-and-click`, async ({ | ||||
|     await scene.expectPixelColor([74, 74, 74], testPoint, 15) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| const loftPointAndClickCases = [ | ||||
|   { shouldPreselect: true }, | ||||
|   { shouldPreselect: false }, | ||||
| ] | ||||
| loftPointAndClickCases.forEach(({ shouldPreselect }) => { | ||||
|   test(`Loft point-and-click (preselected sketches: ${shouldPreselect})`, async ({ | ||||
|     context, | ||||
|     page, | ||||
|     homePage, | ||||
|     scene, | ||||
|     editor, | ||||
|     toolbar, | ||||
|     cmdBar, | ||||
|   }) => { | ||||
|     const initialCode = `sketch001 = startSketchOn('XZ') | ||||
|     |> circle({ center = [0, 0], radius = 30 }, %) | ||||
|     plane001 = offsetPlane('XZ', 50) | ||||
|     sketch002 = startSketchOn(plane001) | ||||
|     |> circle({ center = [0, 0], radius = 20 }, %) | ||||
| ` | ||||
|     await 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 | ||||
|     ) | ||||
|     const loftDeclaration = 'loft001 = loft([sketch001, sketch002])' | ||||
|  | ||||
|     await test.step(`Look for the white of the sketch001 shape`, async () => { | ||||
|       await scene.expectPixelColor([254, 254, 254], testPoint, 15) | ||||
|     }) | ||||
|  | ||||
|     async function selectSketches() { | ||||
|       await clickOnSketch1() | ||||
|       await page.keyboard.down('Shift') | ||||
|       await clickOnSketch2() | ||||
|       await page.waitForTimeout(500) | ||||
|       await page.keyboard.up('Shift') | ||||
|     } | ||||
|  | ||||
|     if (!shouldPreselect) { | ||||
|       await test.step(`Go through the command bar flow without preselected sketches`, async () => { | ||||
|         await toolbar.loftButton.click() | ||||
|         await cmdBar.expectState({ | ||||
|           stage: 'arguments', | ||||
|           currentArgKey: 'selection', | ||||
|           currentArgValue: '', | ||||
|           headerArguments: { Selection: '' }, | ||||
|           highlightedHeaderArg: 'selection', | ||||
|           commandName: 'Loft', | ||||
|         }) | ||||
|         await selectSketches() | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await cmdBar.expectState({ | ||||
|           stage: 'review', | ||||
|           headerArguments: { Selection: '2 faces' }, | ||||
|           commandName: 'Loft', | ||||
|         }) | ||||
|         await cmdBar.progressCmdBar() | ||||
|       }) | ||||
|     } else { | ||||
|       await test.step(`Preselect the two sketches`, async () => { | ||||
|         await selectSketches() | ||||
|       }) | ||||
|  | ||||
|       await test.step(`Go through the command bar flow with preselected sketches`, async () => { | ||||
|         await toolbar.loftButton.click() | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await cmdBar.expectState({ | ||||
|           stage: 'review', | ||||
|           headerArguments: { Selection: '2 faces' }, | ||||
|           commandName: 'Loft', | ||||
|         }) | ||||
|         await cmdBar.progressCmdBar() | ||||
|       }) | ||||
|     } | ||||
|  | ||||
|     await test.step(`Confirm code is added to the editor, scene has changed`, async () => { | ||||
|       await editor.expectEditor.toContain(loftDeclaration) | ||||
|       await editor.expectState({ | ||||
|         diagnostics: [], | ||||
|         activeLines: [loftDeclaration], | ||||
|         highlightedCode: '', | ||||
|       }) | ||||
|       await scene.expectPixelColor([89, 89, 89], testPoint, 15) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| const shellPointAndClickCapCases = [ | ||||
|   { shouldPreselect: true }, | ||||
|   { shouldPreselect: false }, | ||||
| ] | ||||
| shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { | ||||
|   test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({ | ||||
|     context, | ||||
|     page, | ||||
|     homePage, | ||||
|     scene, | ||||
|     editor, | ||||
|     toolbar, | ||||
|     cmdBar, | ||||
|   }) => { | ||||
|     // TODO: fix this test on windows after the electron migration | ||||
|     test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|     const initialCode = `sketch001 = startSketchOn('XZ') | ||||
|     |> circle({ center = [0, 0], radius = 30 }, %) | ||||
|     extrude001 = extrude(30, sketch001) | ||||
|     ` | ||||
|     await context.addInitScript((initialCode) => { | ||||
|       localStorage.setItem('persistCode', initialCode) | ||||
|     }, initialCode) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|  | ||||
|     // One dumb hardcoded screen pixel value | ||||
|     const testPoint = { x: 575, y: 200 } | ||||
|     const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y) | ||||
|     const shellDeclaration = | ||||
|       "shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)" | ||||
|  | ||||
|     await test.step(`Look for the grey of the shape`, async () => { | ||||
|       await scene.expectPixelColor([127, 127, 127], testPoint, 15) | ||||
|     }) | ||||
|  | ||||
|     if (!shouldPreselect) { | ||||
|       await test.step(`Go through the command bar flow without preselected faces`, async () => { | ||||
|         await toolbar.shellButton.click() | ||||
|         await cmdBar.expectState({ | ||||
|           stage: 'arguments', | ||||
|           currentArgKey: 'selection', | ||||
|           currentArgValue: '', | ||||
|           headerArguments: { | ||||
|             Selection: '', | ||||
|             Thickness: '', | ||||
|           }, | ||||
|           highlightedHeaderArg: 'selection', | ||||
|           commandName: 'Shell', | ||||
|         }) | ||||
|         await clickOnCap() | ||||
|         await page.waitForTimeout(500) | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await cmdBar.expectState({ | ||||
|           stage: 'review', | ||||
|           headerArguments: { | ||||
|             Selection: '1 cap', | ||||
|             Thickness: '5', | ||||
|           }, | ||||
|           commandName: 'Shell', | ||||
|         }) | ||||
|         await cmdBar.progressCmdBar() | ||||
|       }) | ||||
|     } else { | ||||
|       await test.step(`Preselect the cap`, async () => { | ||||
|         await clickOnCap() | ||||
|         await page.waitForTimeout(500) | ||||
|       }) | ||||
|  | ||||
|       await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => { | ||||
|         await toolbar.shellButton.click() | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await cmdBar.expectState({ | ||||
|           stage: 'review', | ||||
|           headerArguments: { | ||||
|             Selection: '1 cap', | ||||
|             Thickness: '5', | ||||
|           }, | ||||
|           commandName: 'Shell', | ||||
|         }) | ||||
|         await cmdBar.progressCmdBar() | ||||
|       }) | ||||
|     } | ||||
|  | ||||
|     await test.step(`Confirm code is added to the editor, scene has changed`, async () => { | ||||
|       await editor.expectEditor.toContain(shellDeclaration) | ||||
|       await editor.expectState({ | ||||
|         diagnostics: [], | ||||
|         activeLines: [shellDeclaration], | ||||
|         highlightedCode: '', | ||||
|       }) | ||||
|       await scene.expectPixelColor([146, 146, 146], testPoint, 15) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test('Shell point-and-click wall', async ({ | ||||
|   context, | ||||
|   page, | ||||
|   homePage, | ||||
|   scene, | ||||
|   editor, | ||||
|   toolbar, | ||||
|   cmdBar, | ||||
| }) => { | ||||
|   const initialCode = `sketch001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-20, 20], %) | ||||
|   |> xLine(40, %) | ||||
|   |> yLine(-60, %) | ||||
|   |> xLine(-40, %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| extrude001 = extrude(40, sketch001) | ||||
|   ` | ||||
|   await context.addInitScript((initialCode) => { | ||||
|     localStorage.setItem('persistCode', initialCode) | ||||
|   }, initialCode) | ||||
|   await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|   await homePage.goToModelingScene() | ||||
|  | ||||
|   // One dumb hardcoded screen pixel value | ||||
|   const testPoint = { x: 580, y: 180 } | ||||
|   const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y) | ||||
|   const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70) | ||||
|   const mutatedCode = 'xLine(-40, %, $seg01)' | ||||
|   const shellDeclaration = | ||||
|     "shell001 = shell({  faces = ['end', seg01],  thickness = 5}, extrude001)" | ||||
|   const formattedOutLastLine = '}, extrude001)' | ||||
|  | ||||
|   await test.step(`Look for the grey of the shape`, async () => { | ||||
|     await scene.expectPixelColor([99, 99, 99], testPoint, 15) | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Go through the command bar flow, selecting a wall and keeping default thickness`, async () => { | ||||
|     await toolbar.shellButton.click() | ||||
|     await cmdBar.expectState({ | ||||
|       stage: 'arguments', | ||||
|       currentArgKey: 'selection', | ||||
|       currentArgValue: '', | ||||
|       headerArguments: { | ||||
|         Selection: '', | ||||
|         Thickness: '', | ||||
|       }, | ||||
|       highlightedHeaderArg: 'selection', | ||||
|       commandName: 'Shell', | ||||
|     }) | ||||
|     await clickOnCap() | ||||
|     await page.keyboard.down('Shift') | ||||
|     await clickOnWall() | ||||
|     await page.waitForTimeout(500) | ||||
|     await page.keyboard.up('Shift') | ||||
|     await cmdBar.progressCmdBar() | ||||
|     await cmdBar.progressCmdBar() | ||||
|     await cmdBar.expectState({ | ||||
|       stage: 'review', | ||||
|       headerArguments: { | ||||
|         Selection: '1 cap, 1 face', | ||||
|         Thickness: '5', | ||||
|       }, | ||||
|       commandName: 'Shell', | ||||
|     }) | ||||
|     await cmdBar.progressCmdBar() | ||||
|   }) | ||||
|  | ||||
|   await test.step(`Confirm code is added to the editor, scene has changed`, async () => { | ||||
|     await editor.expectEditor.toContain(mutatedCode) | ||||
|     await editor.expectEditor.toContain(shellDeclaration) | ||||
|     await editor.expectState({ | ||||
|       diagnostics: [], | ||||
|       activeLines: [formattedOutLastLine], | ||||
|       highlightedCode: '', | ||||
|     }) | ||||
|     await scene.expectPixelColor([49, 49, 49], testPoint, 15) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
							
								
								
									
										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() | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
| @ -1,46 +1,37 @@ | ||||
| import { test, expect, Page } from '@playwright/test' | ||||
| import { join } from 'path' | ||||
| import { test, expect, Page } from './zoo-test' | ||||
| import path from 'path' | ||||
| import * as fsp from 'fs/promises' | ||||
| import { | ||||
|   getUtils, | ||||
|   setup, | ||||
|   setupElectron, | ||||
|   tearDown, | ||||
|   executorInputPath, | ||||
| } from './test-utils' | ||||
| import { getUtils, executorInputPath } from './test-utils' | ||||
| import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates' | ||||
| import { bracket } from 'lib/exampleKcl' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }, testInfo) => { | ||||
|   await setup(context, page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.describe('Regression tests', () => { | ||||
|   // bugs we found that don't fit neatly into other categories | ||||
|   test('bad model has inline error #3251', async ({ page }) => { | ||||
|   test('bad model has inline error #3251', async ({ | ||||
|     context, | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     // because the model has `line([0,0]..` it is valid code, but the model is invalid | ||||
|     // regression test for https://github.com/KittyCAD/modeling-app/issues/3251 | ||||
|     // Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|     await context.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch2 = startSketchOn("XY") | ||||
| sketch001 = startSketchAt([-0, -0]) | ||||
|   |> line([0, 0], %) | ||||
|   |> line([-4.84, -5.29], %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%)` | ||||
|   sketch001 = startSketchAt([-0, -0]) | ||||
|     |> line([0, 0], %) | ||||
|     |> line([-4.84, -5.29], %) | ||||
|     |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|     |> close(%)` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.waitForPageLoad() | ||||
|  | ||||
|     // error in guter | ||||
|     await expect(page.locator('.cm-lint-marker-error')).toBeVisible() | ||||
| @ -56,6 +47,7 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|   }) | ||||
|   test('user should not have to press down twice in cmdbar', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     // because the model has `line([0,0]..` it is valid code, but the model is invalid | ||||
|     // regression test for https://github.com/KittyCAD/modeling-app/issues/3251 | ||||
| @ -64,26 +56,38 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch2 = startSketchOn("XY") | ||||
| sketch001 = startSketchAt([-0, -0]) | ||||
|   |> line([0, 0], %) | ||||
|   |> line([-4.84, -5.29], %) | ||||
|         `sketch001 = startSketchOn('XY') | ||||
|   |> startProfileAt([82.33, 238.21], %) | ||||
|   |> angledLine([0, 288.63], %, $rectangleSegmentA001) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001) - 90, | ||||
|        197.97 | ||||
|      ], %, $rectangleSegmentB001) | ||||
|   |> angledLine([ | ||||
|        segAng(rectangleSegmentA001), | ||||
|        -segLen(rectangleSegmentA001) | ||||
|      ], %, $rectangleSegmentC001) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%)` | ||||
|   |> close(%) | ||||
| extrude001 = extrude(50, sketch001) | ||||
| ` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await page.goto('/') | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.waitForPageLoad() | ||||
|  | ||||
|     await test.step('Check arrow down works', async () => { | ||||
|       await page.getByTestId('command-bar-open-button').hover() | ||||
|       await page.getByTestId('command-bar-open-button').click() | ||||
|  | ||||
|       await page | ||||
|         .getByRole('option', { name: 'floppy disk arrow Export' }) | ||||
|         .click() | ||||
|       const floppy = page.getByRole('option', { | ||||
|         name: 'floppy disk arrow Export', | ||||
|       }) | ||||
|  | ||||
|       await floppy.click() | ||||
|  | ||||
|       // press arrow down key twice | ||||
|       await page.keyboard.press('ArrowDown') | ||||
| @ -115,21 +119,22 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|       ) | ||||
|     }) | ||||
|   }) | ||||
|   test('executes on load', async ({ page }) => { | ||||
|   test('executes on load', async ({ page, homePage }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `sketch001 = startSketchOn('-XZ') | ||||
|     |> startProfileAt([-6.95, 4.98], %) | ||||
|     |> line([25.1, 0.41], %) | ||||
|     |> line([0.73, -14.93], %) | ||||
|     |> line([-23.44, 0.52], %)` | ||||
|   |> startProfileAt([-6.95, 4.98], %) | ||||
|   |> line([25.1, 0.41], %) | ||||
|   |> line([0.73, -14.93], %) | ||||
|   |> line([-23.44, 0.52], %)` | ||||
|       ) | ||||
|     }) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.waitForPageLoad() | ||||
|  | ||||
|     // expand variables section | ||||
|     const variablesTabButton = page.getByTestId('variables-pane-button') | ||||
| @ -148,14 +153,15 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|     ).toBeVisible() | ||||
|   }) | ||||
|  | ||||
|   test('re-executes', async ({ page }) => { | ||||
|   test('re-executes', async ({ page, homePage }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem('persistCode', `myVar = 5`) | ||||
|     }) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.waitForPageLoad() | ||||
|  | ||||
|     const variablesTabButton = page.getByTestId('variables-pane-button') | ||||
|     await variablesTabButton.click() | ||||
| @ -174,32 +180,33 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|       page.locator('.pretty-json-container >> text=myVar:67') | ||||
|     ).toBeVisible() | ||||
|   }) | ||||
|   test('ProgramMemory can be serialised', async ({ page }) => { | ||||
|   test('ProgramMemory can be serialised', async ({ page, homePage }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `part = startSketchOn('XY') | ||||
|     |> startProfileAt([0, 0], %) | ||||
|     |> line([0, 1], %) | ||||
|     |> line([1, 0], %) | ||||
|     |> line([0, -1], %) | ||||
|     |> close(%) | ||||
|     |> extrude(1, %) | ||||
|     |> patternLinear3d({ | ||||
|           axis: [1, 0, 1], | ||||
|           repetitions: 3, | ||||
|           distance: 6 | ||||
|         }, %)` | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([0, 1], %) | ||||
|   |> line([1, 0], %) | ||||
|   |> line([0, -1], %) | ||||
|   |> close(%) | ||||
|   |> extrude(1, %) | ||||
|   |> patternLinear3d({ | ||||
|         axis: [1, 0, 1], | ||||
|         repetitions: 3, | ||||
|         distance: 6 | ||||
|       }, %)` | ||||
|       ) | ||||
|     }) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     const messages: string[] = [] | ||||
|  | ||||
|     // Listen for all console events and push the message text to an array | ||||
|     page.on('console', (message) => messages.push(message.text())) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.waitForPageLoad() | ||||
|  | ||||
|     // wait for execution done | ||||
|     await u.openDebugPanel() | ||||
| @ -212,19 +219,26 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|       }) | ||||
|     }) | ||||
|   }) | ||||
|   test('ensure the Zoo logo is not a link in browser app', async ({ page }) => { | ||||
|  | ||||
|   // Not relevant to us anymore, or at least for the time being. | ||||
|   test.skip('ensure the Zoo logo is not a link in browser app', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.waitForPageLoad() | ||||
|  | ||||
|     const zooLogo = page.locator('[data-testid="app-logo"]') | ||||
|     // Make sure it's not a link | ||||
|     await expect(zooLogo).not.toHaveAttribute('href') | ||||
|   }) | ||||
|  | ||||
|   test( | ||||
|     'Position _ Is Out Of Range... regression test', | ||||
|     { tag: ['@skipWin'] }, | ||||
|     async ({ page }) => { | ||||
|     async ({ context, page, homePage }) => { | ||||
|       // SKip on windows, its being weird. | ||||
|       test.skip( | ||||
|         process.platform === 'win32', | ||||
| @ -233,25 +247,26 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|  | ||||
|       const u = await getUtils(page) | ||||
|       // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       await page.addInitScript(async () => { | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|       await context.addInitScript(async () => { | ||||
|         localStorage.setItem( | ||||
|           'persistCode', | ||||
|           `exampleSketch = startSketchOn("XZ") | ||||
|     |> startProfileAt([0, 0], %) | ||||
|     |> angledLine({ angle: 50, length: 45 }, %) | ||||
|     |> yLineTo(0, %) | ||||
|     |> close(%) | ||||
|     |> | ||||
|  | ||||
|   example = extrude(5, exampleSketch) | ||||
|   shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)` | ||||
|       |> startProfileAt([0, 0], %) | ||||
|       |> angledLine({ angle: 50, length: 45 }, %) | ||||
|       |> yLineTo(0, %) | ||||
|       |> close(%) | ||||
|       |> | ||||
|    | ||||
|     example = extrude(5, exampleSketch) | ||||
|     shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)` | ||||
|         ) | ||||
|       }) | ||||
|  | ||||
|       await expect(async () => { | ||||
|         await page.goto('/') | ||||
|         await homePage.goToModelingScene() | ||||
|         await u.waitForPageLoad() | ||||
|  | ||||
|         // error in guter | ||||
|         await expect(page.locator('.cm-lint-marker-error')).toBeVisible({ | ||||
|           timeout: 1_000, | ||||
| @ -293,12 +308,12 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|  | ||||
|       await expect(page.locator('.cm-content')) | ||||
|         .toContainText(`exampleSketch = startSketchOn("XZ") | ||||
|     |> startProfileAt([0, 0], %) | ||||
|     |> angledLine({ angle: 50, length: 45 }, %) | ||||
|     |> yLineTo(0, %) | ||||
|     |> close(%) | ||||
|  | ||||
|     thing: "blah"`) | ||||
|       |> startProfileAt([0, 0], %) | ||||
|       |> angledLine({ angle: 50, length: 45 }, %) | ||||
|       |> yLineTo(0, %) | ||||
|       |> close(%) | ||||
|    | ||||
|       thing: "blah"`) | ||||
|  | ||||
|       await expect(page.locator('.cm-lint-marker-error')).toBeVisible() | ||||
|     } | ||||
| @ -306,6 +321,7 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|  | ||||
|   test('when engine fails export we handle the failure and alert the user', async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript( | ||||
| @ -316,9 +332,10 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|       { code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } | ||||
|     ) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await homePage.goToModelingScene() | ||||
|     await u.waitForPageLoad() | ||||
|  | ||||
|     // wait for execution done | ||||
|     await u.openDebugPanel() | ||||
| @ -374,7 +391,6 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|  | ||||
|     // wait for execution done | ||||
|     await u.openDebugPanel() | ||||
|     await u.clearCommandLogs() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.closeDebugPanel() | ||||
|  | ||||
| @ -408,7 +424,7 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|   test( | ||||
|     'ensure you can not export while an export is already going', | ||||
|     { tag: ['@skipLinux', '@skipWin'] }, | ||||
|     async ({ page }) => { | ||||
|     async ({ page, homePage }) => { | ||||
|       // This is being weird on ubuntu and windows. | ||||
|       test.skip( | ||||
|         // eslint-disable-next-line jest/valid-title | ||||
| @ -428,9 +444,10 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|           } | ||||
|         ) | ||||
|  | ||||
|         await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|         await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||
|  | ||||
|         await u.waitForAuthSkipAppStart() | ||||
|         await homePage.goToModelingScene() | ||||
|         await u.waitForPageLoad() | ||||
|  | ||||
|         // wait for execution done | ||||
|         await u.openDebugPanel() | ||||
| @ -500,20 +517,17 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|   test( | ||||
|     `Network health indicator only appears in modeling view`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browserName: _ }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async (dir) => { | ||||
|           const bracketDir = join(dir, 'bracket') | ||||
|           await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|           await fsp.copyFile( | ||||
|             executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|             join(bracketDir, 'main.kcl') | ||||
|           ) | ||||
|         }, | ||||
|     async ({ context, page }, testInfo) => { | ||||
|       await context.folderSetupFn(async (dir) => { | ||||
|         const bracketDir = path.join(dir, 'bracket') | ||||
|         await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|         await fsp.copyFile( | ||||
|           executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|           path.join(bracketDir, 'main.kcl') | ||||
|         ) | ||||
|       }) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|       const u = await getUtils(page) | ||||
|  | ||||
|       // Locators | ||||
| @ -539,14 +553,15 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|         await u.waitForPageLoad() | ||||
|         await expect(networkHealthIndicator).toContainText('Connected') | ||||
|       }) | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test(`View gizmo stays visible even when zoomed out all the way`, async ({ | ||||
|     page, | ||||
|     homePage, | ||||
|   }) => { | ||||
|     // TODO: fix this test on windows after the electron migration | ||||
|     test.skip(process.platform === 'win32', 'Skip on windows') | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|     // Constants and locators | ||||
| @ -561,8 +576,9 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|       await page.addInitScript(async () => { | ||||
|         localStorage.setItem('persistCode', '') | ||||
|       }) | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       await u.waitForAuthSkipAppStart() | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|       await homePage.goToModelingScene() | ||||
|       await u.waitForPageLoad() | ||||
|       await u.closeKclCodePanel() | ||||
|     }) | ||||
|  | ||||
| @ -572,7 +588,7 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|           timeout: 5000, | ||||
|           message: 'Plane color is visible', | ||||
|         }) | ||||
|         .toBeLessThan(15) | ||||
|         .toBeLessThanOrEqual(15) | ||||
|  | ||||
|       let maxZoomOuts = 10 | ||||
|       let middlePixelIsBackgroundColor = | ||||
| @ -590,7 +606,7 @@ sketch001 = startSketchAt([-0, -0]) | ||||
|       } | ||||
|  | ||||
|       expect(middlePixelIsBackgroundColor, { | ||||
|         message: 'We no longer the default planes', | ||||
|         message: 'We should not see the default planes', | ||||
|       }).toBeTruthy() | ||||
|     }) | ||||
|  | ||||
|  | ||||
| @ -7,6 +7,8 @@ try { | ||||
|     .split('\n') | ||||
|     .filter((line) => line && line.length > 1) | ||||
|     .forEach((line) => { | ||||
|       // Allow line comments. | ||||
|       if (line.trimStart().startsWith('#')) return | ||||
|       const [key, value] = line.split('=') | ||||
|       // prefer env vars over secrets file | ||||
|       secrets[key] = process.env[key] || (value as any).replaceAll('"', '') | ||||
|  | ||||
| @ -47,7 +47,11 @@ test.beforeEach(async ({ page }) => { | ||||
|  | ||||
| test.setTimeout(60_000) | ||||
|  | ||||
| test( | ||||
| // We test this end to end already - getting this to work on web just to take | ||||
| // a snapshot of it feels weird. I'd rather our regular tests fail. | ||||
| // The primary failure is doExport now relies on the filesystem. We can follow | ||||
| // up with another PR if we want this back. | ||||
| test.skip( | ||||
|   'exports of each format should work', | ||||
|   { tag: ['@snapshot', '@skipWin', '@skipMacos'] }, | ||||
|   async ({ page, context }) => { | ||||
| @ -371,6 +375,7 @@ const extrudeDefaultPlane = async (context: any, page: any, plane: string) => { | ||||
|   await u.closeKclCodePanel() | ||||
|   await expect(page).toHaveScreenshot({ | ||||
|     maxDiffPixels: 100, | ||||
|     mask: [page.getByTestId('model-state-indicator')], | ||||
|   }) | ||||
|   await u.openKclCodePanel() | ||||
| } | ||||
| @ -950,7 +955,75 @@ test( | ||||
|  | ||||
| test.describe('Grid visibility', { tag: '@snapshot' }, () => { | ||||
|   // FIXME: Skip on macos its being weird. | ||||
|   test.skip(process.platform === 'darwin', 'Skip on macos') | ||||
|   // test.skip(process.platform === 'darwin', 'Skip on macos') | ||||
|  | ||||
|   test('Grid turned off to on via command bar', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|     const stream = page.getByTestId('stream') | ||||
|     const mask = [ | ||||
|       page.locator('#app-header'), | ||||
|       page.locator('#sidebar-top-ribbon'), | ||||
|       page.locator('#sidebar-bottom-ribbon'), | ||||
|     ] | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await page.goto('/') | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     // wait for execution done | ||||
|     await expect( | ||||
|       page.locator('[data-message-type="execution-done"]') | ||||
|     ).toHaveCount(1) | ||||
|     await u.closeDebugPanel() | ||||
|     await u.closeKclCodePanel() | ||||
|     // TODO: Find a way to truly know that the objects have finished | ||||
|     // rendering, because an execution-done message is not sufficient. | ||||
|     await page.waitForTimeout(1000) | ||||
|  | ||||
|     // Open the command bar. | ||||
|     await page | ||||
|       .getByRole('button', { name: 'Commands', exact: false }) | ||||
|       .or(page.getByRole('button', { name: '⌘K' })) | ||||
|       .click() | ||||
|     const commandName = 'show scale grid' | ||||
|     const commandOption = page.getByRole('option', { | ||||
|       name: commandName, | ||||
|       exact: false, | ||||
|     }) | ||||
|     const cmdSearchBar = page.getByPlaceholder('Search commands') | ||||
|     // This selector changes after we set the setting | ||||
|     await cmdSearchBar.fill(commandName) | ||||
|     await expect(commandOption).toBeVisible() | ||||
|     await commandOption.click() | ||||
|  | ||||
|     const toggleInput = page.getByPlaceholder('Off') | ||||
|     await expect(toggleInput).toBeVisible() | ||||
|     await expect(toggleInput).toBeFocused() | ||||
|  | ||||
|     // Select On | ||||
|     await page.keyboard.press('ArrowDown') | ||||
|     await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute( | ||||
|       'data-headlessui-state', | ||||
|       'active selected' | ||||
|     ) | ||||
|     await page.keyboard.press('ArrowUp') | ||||
|     await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute( | ||||
|       'data-headlessui-state', | ||||
|       'active' | ||||
|     ) | ||||
|     await page.keyboard.press('Enter') | ||||
|  | ||||
|     // Check the toast appeared | ||||
|     await expect( | ||||
|       page.getByText(`Set show scale grid to "true" as a user default`) | ||||
|     ).toBeVisible() | ||||
|  | ||||
|     await expect(stream).toHaveScreenshot({ | ||||
|       maxDiffPixels: 100, | ||||
|       mask, | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   test('Grid turned off', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
| @ -1096,3 +1169,109 @@ test.fixme('theme persists', async ({ page, context }) => { | ||||
|     maxDiffPixels: 100, | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| test.describe('code color goober', { tag: '@snapshot' }, () => { | ||||
|   test('code color goober', async ({ page, context }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await context.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `// Create a pipe using a sweep. | ||||
|  | ||||
| // Create a path for the sweep. | ||||
| sweepPath = startSketchOn('XZ') | ||||
|   |> startProfileAt([0.05, 0.05], %) | ||||
|   |> line([0, 7], %) | ||||
|   |> tangentialArc({ offset = 90, radius = 5 }, %) | ||||
|   |> line([-3, 0], %) | ||||
|   |> tangentialArc({ offset = -90, radius = 5 }, %) | ||||
|   |> line([0, 7], %) | ||||
|  | ||||
| sweepSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([2, 0], %) | ||||
|   |> arc({ | ||||
|        angleEnd = 360, | ||||
|        angleStart = 0, | ||||
|        radius = 2 | ||||
|      }, %) | ||||
|   |> sweep({ | ||||
|     path = sweepPath, | ||||
|   }, %) | ||||
|   |> appearance({ | ||||
|        color = "#bb00ff", | ||||
|        metalness = 90, | ||||
|        roughness = 90 | ||||
|      }, %) | ||||
| ` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 1000 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.clearAndCloseDebugPanel() | ||||
|  | ||||
|     await expect(page, 'expect small color widget').toHaveScreenshot({ | ||||
|       maxDiffPixels: 100, | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   test('code color goober opening window', async ({ page, context }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await context.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `// Create a pipe using a sweep. | ||||
|  | ||||
| // Create a path for the sweep. | ||||
| sweepPath = startSketchOn('XZ') | ||||
|   |> startProfileAt([0.05, 0.05], %) | ||||
|   |> line([0, 7], %) | ||||
|   |> tangentialArc({ offset = 90, radius = 5 }, %) | ||||
|   |> line([-3, 0], %) | ||||
|   |> tangentialArc({ offset = -90, radius = 5 }, %) | ||||
|   |> line([0, 7], %) | ||||
|  | ||||
| sweepSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([2, 0], %) | ||||
|   |> arc({ | ||||
|        angleEnd = 360, | ||||
|        angleStart = 0, | ||||
|        radius = 2 | ||||
|      }, %) | ||||
|   |> sweep({ | ||||
|     path = sweepPath, | ||||
|   }, %) | ||||
|   |> appearance({ | ||||
|        color = "#bb00ff", | ||||
|        metalness = 90, | ||||
|        roughness = 90 | ||||
|      }, %) | ||||
| ` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 1000 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.clearAndCloseDebugPanel() | ||||
|  | ||||
|     await expect(page.locator('.cm-css-color-picker-wrapper')).toBeVisible() | ||||
|  | ||||
|     // Click the color widget | ||||
|     await page.locator('.cm-css-color-picker-wrapper input').click() | ||||
|  | ||||
|     await expect( | ||||
|       page, | ||||
|       'expect small color widget to have window open' | ||||
|     ).toHaveScreenshot({ | ||||
|       maxDiffPixels: 100, | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 51 KiB | 
| Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB | 
| Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB | 
| Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 53 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB | 
| After Width: | Height: | Size: 43 KiB | 
