Compare commits
	
		
			154 Commits
		
	
	
		
			nightly-v2
			...
			pierremtb/
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fecf5c2ee7 | |||
| 8ef31a0be1 | |||
| 3adb42b5f2 | |||
| 20016b101e | |||
| 8d9dbf36c3 | |||
| 440704ed9f | |||
| 2261217a5d | |||
| 10da986649 | |||
| 10789d9c3c | |||
| 67cc4f5835 | |||
| 2692f2b73a | |||
| 965cb18059 | |||
| a022b8ef6c | |||
| 4d24bf7c94 | |||
| 9a537da183 | |||
| df81b76b8b | |||
| ac3f7ab712 | |||
| d531728675 | |||
| 1d78fc15ac | |||
| c32aebc8ad | |||
| 997ebce3eb | |||
| dac91d3b79 | |||
| 1eaf371b44 | |||
| 0698432abf | |||
| 54da18d8ab | |||
| 2fe5ef7034 | |||
| 16b5eeadb1 | |||
| 7be4001839 | |||
| ffb2559787 | |||
| 0592d3b5da | |||
| 31e4d60045 | |||
| c0817b00e4 | |||
| 4ea1d16fb6 | |||
| d049bf33e8 | |||
| 7b11047d07 | |||
| 412e9568f2 | |||
| 9be208e5e1 | |||
| 842ef5ede9 | |||
| 3f855d7bad | |||
| 0a1a6e50cf | |||
| d4e955289c | |||
| c147a219f4 | |||
| 38513a1e25 | |||
| c0c5c790ca | |||
| 8b60f75220 | |||
| f91ad4331f | |||
| 59103a2118 | |||
| 9737c2550a | |||
| bf9d01a8dd | |||
| 702e322f90 | |||
| e82830754d | |||
| 7806377a5a | |||
| 859afa2fd8 | |||
| 0a5f3093fc | |||
| b65f7939f6 | |||
| c35dea5e07 | |||
| fc66d4745f | |||
| b313d26c2a | |||
| 00b94ead62 | |||
| 0531ea1ce9 | |||
| 5f9a4887c1 | |||
| da7dfa16d8 | |||
| 363ae10658 | |||
| ac4a6c84cf | |||
| c6fad2e2dc | |||
| 013cb10961 | |||
| 6261083cb1 | |||
| 2b0ba37ed0 | |||
| 96174f3cf6 | |||
| aed62ff912 | |||
| 9334d64608 | |||
| 4fa7d2d8c8 | |||
| 3e615dfdbc | |||
| c9860af29f | |||
| 23a42f0195 | |||
| a77fa639f3 | |||
| 0a5ad7c95b | |||
| 4a654523d2 | |||
| 73a7e2bfd6 | |||
| eb0850fea9 | |||
| 029f76f273 | |||
| 28b5f7080c | |||
| 5b1dcfecd6 | |||
| f89d191425 | |||
| 2f4e4b62a8 | |||
| 5ebd5c8dbb | |||
| a9ceaf2678 | |||
| c8afd3399b | |||
| 5dda4828c6 | |||
| 72acab752c | |||
| 81df38ad1c | |||
| 0576a2bef1 | |||
| 4b2f6b4647 | |||
| 69edaa4183 | |||
| 2eb7c382bf | |||
| 38913ecb98 | |||
| debd06129f | |||
| d38bd342a0 | |||
| f026f10335 | |||
| 895d7ebc6d | |||
| 65edf17a44 | |||
| 0c2a0a8c07 | |||
| 97cef4d16c | |||
| 9358278f7b | |||
| a174e084d4 | |||
| df7246897a | |||
| 0c9f64dd7c | |||
| d2b9d3a058 | |||
| 7e54f08778 | |||
| d9c2dd376e | |||
| 275a2150e7 | |||
| 8b8feb8d68 | |||
| e21ef3f122 | |||
| 66834931aa | |||
| 06c1bcaf2e | |||
| fc7df7ecbe | |||
| 67cb7b33bb | |||
| ea74b94fac | |||
| 529833c63f | |||
| 92da86391a | |||
| e7cb390db4 | |||
| 8a66bbbdbd | |||
| 3c53babb50 | |||
| 474acb1c68 | |||
| 1c941112d7 | |||
| 6f1d718097 | |||
| 36957237c0 | |||
| da9cae98aa | |||
| 9ae025dc56 | |||
| 579ab23d78 | |||
| 4bef33e745 | |||
| ac7bd28c5a | |||
| d478d81156 | |||
| 3d27f0191b | |||
| 30c2acd18a | |||
| a83b4b2145 | |||
| 70b8541038 | |||
| bb51646738 | |||
| c02e31a530 | |||
| 1d06cc7845 | |||
| e0c07eecfe | |||
| c5d42500fa | |||
| e6e47f77f0 | |||
| 662c2485ac | |||
| 9f891deebb | |||
| d08a07a1f8 | |||
| 872b196a86 | |||
| d535a2862d | |||
| 63a3bc7bc6 | |||
| 02055a8b31 | |||
| 93891422f7 | |||
| 7193b4110a | |||
| 76e7d80a55 | |||
| b816df21d2 | 
| @ -1,3 +1,3 @@ | |||||||
| [codespell] | [codespell] | ||||||
| ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall | ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser | ||||||
| skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts | skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,tsconfig.tsbuildinfo | ||||||
|  | |||||||
							
								
								
									
										28
									
								
								.eslintrc
									
									
									
									
									
								
							
							
						
						| @ -5,16 +5,32 @@ | |||||||
|     }, |     }, | ||||||
|     "plugins": [ |     "plugins": [ | ||||||
|       "css-modules", |       "css-modules", | ||||||
|  |       "jest", | ||||||
|  |       "jsx-a11y", | ||||||
|  |       "react", | ||||||
|  |       "react-hooks", | ||||||
|       "suggest-no-throw", |       "suggest-no-throw", | ||||||
|  |       "testing-library", | ||||||
|  |       "@typescript-eslint" | ||||||
|     ], |     ], | ||||||
|     "extends": [ |     "extends": [ | ||||||
|       "react-app", |       "plugin:css-modules/recommended", | ||||||
|       "react-app/jest", |       "plugin:jsx-a11y/recommended", | ||||||
|       "plugin:css-modules/recommended" |       "plugin:react-hooks/recommended" | ||||||
|     ], |     ], | ||||||
|     "rules": { |     "rules": { | ||||||
|       "@typescript-eslint/no-floating-promises": "error", |       "@typescript-eslint/no-floating-promises": "error", | ||||||
|       "@typescript-eslint/no-misused-promises": "error", |       "@typescript-eslint/no-misused-promises": "error", | ||||||
|  |       "jsx-a11y/click-events-have-key-events": "off", | ||||||
|  |       "jsx-a11y/no-autofocus": "off", | ||||||
|  |       "jsx-a11y/no-noninteractive-element-interactions": "off", | ||||||
|  |       "no-restricted-globals": [ | ||||||
|  |         "error", | ||||||
|  |         { | ||||||
|  |           "name": "isNaN", | ||||||
|  |           "message": "Use Number.isNaN() instead." | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|       "semi": [ |       "semi": [ | ||||||
|         "error", |         "error", | ||||||
|         "never" |         "never" | ||||||
| @ -25,6 +41,9 @@ | |||||||
|     "overrides": [ |     "overrides": [ | ||||||
|       { |       { | ||||||
|         "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure |         "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure | ||||||
|  |         "extends": [ | ||||||
|  |           "plugin:testing-library/react" | ||||||
|  |         ], | ||||||
|         "rules": { |         "rules": { | ||||||
|           "suggest-no-throw/suggest-no-throw": "off", |           "suggest-no-throw/suggest-no-throw": "off", | ||||||
|           "testing-library/prefer-screen-queries": "off", |           "testing-library/prefer-screen-queries": "off", | ||||||
| @ -33,6 +52,9 @@ | |||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         "files": ["src/**/*.test.ts"], |         "files": ["src/**/*.test.ts"], | ||||||
|  |         "extends": [ | ||||||
|  |           "plugin:testing-library/react" | ||||||
|  |         ], | ||||||
|         "rules": { |         "rules": { | ||||||
|           "suggest-no-throw/suggest-no-throw": "off", |           "suggest-no-throw/suggest-no-throw": "off", | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -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 |  | ||||||
							
								
								
									
										22
									
								
								.github/ci-cd-scripts/playwright-electron.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -1,15 +1,17 @@ | |||||||
|  | #!/bin/bash | ||||||
|  |  | ||||||
| # bash strict mode | # bash strict mode | ||||||
| set -euo pipefail | set -euo pipefail | ||||||
|  |  | ||||||
| if [[ ! -f "test-results/.last-run.json" ]]; then | if [[ ! -f "test-results/.last-run.json" ]]; then | ||||||
|     # if no last run artifact, than run plawright normally |     # if no last run artifact, than run plawright normally | ||||||
|     echo "run playwright normally" |     echo "run playwright normally" | ||||||
|         if [[ "$1" == ubuntu-latest* ]]; then |         if [[ "$3" == *ubuntu* ]]; then | ||||||
|             xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu || true |             xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --shard=$1/$2 || true | ||||||
|         elif [[ "$1" == windows-latest* ]]; then |         elif [[ "$3" == *windows* ]]; then | ||||||
|             yarn test:playwright:electron:windows || true |             yarn test:playwright:electron:windows -- --shard=$1/$2 || true | ||||||
|         elif [[ "$1" == macos-14* ]]; then |         elif [[ "$3" == *macos* ]]; then | ||||||
|             yarn test:playwright:electron:macos || true |             yarn test:playwright:electron:macos  -- --shard=$1/$2 || true | ||||||
|         else |         else | ||||||
|             echo "Do not run playwright. Unable to detect os runtime." |             echo "Do not run playwright. Unable to detect os runtime." | ||||||
|             exit 1 |             exit 1 | ||||||
| @ -19,7 +21,7 @@ if [[ ! -f "test-results/.last-run.json" ]]; then | |||||||
| fi | fi | ||||||
|  |  | ||||||
| retry=1 | retry=1 | ||||||
| max_retrys=4 | max_retrys=5 | ||||||
|  |  | ||||||
| # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues | # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues | ||||||
| while [[ $retry -le $max_retrys ]]; do | while [[ $retry -le $max_retrys ]]; do | ||||||
| @ -28,11 +30,11 @@ while [[ $retry -le $max_retrys ]]; do | |||||||
|         if [[ $failed_tests -gt 0 ]]; then |         if [[ $failed_tests -gt 0 ]]; then | ||||||
|             echo "retried=true" >>$GITHUB_OUTPUT |             echo "retried=true" >>$GITHUB_OUTPUT | ||||||
|             echo "run playwright with last failed tests and retry $retry" |             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 |                 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 |                 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 |                 yarn test:playwright:electron:macos -- --last-failed || true | ||||||
|             else |             else | ||||||
|                 echo "Do not run playwright. Unable to detect os runtime." |                 echo "Do not run playwright. Unable to detect os runtime." | ||||||
|  | |||||||
							
								
								
									
										55
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -5,24 +5,37 @@ | |||||||
|  |  | ||||||
| version: 2 | version: 2 | ||||||
| updates: | updates: | ||||||
|     - package-ecosystem: 'npm' # See documentation for possible values |   - package-ecosystem: 'npm' # See documentation for possible values | ||||||
|       directory: '/' # Location of package manifests |     directories: | ||||||
|       schedule: |       - '/' | ||||||
|           interval: 'weekly' |       - '/packages/codemirror-lang-kcl/' | ||||||
|       reviewers: |       - '/packages/codemirror-lsp-client/' | ||||||
|           - franknoirot |     schedule: | ||||||
|           - irev-dev |       interval: weekly | ||||||
|     - package-ecosystem: 'github-actions' # See documentation for possible values |       day: monday | ||||||
|       directory: '/' # Location of package manifests |     reviewers: | ||||||
|       schedule: |       - franknoirot | ||||||
|           interval: 'weekly' |       - irev-dev | ||||||
|       reviewers: |   - package-ecosystem: 'github-actions' # See documentation for possible values | ||||||
|           - adamchalmers |     directory: '/' # Location of package manifests | ||||||
|           - jessfraz |     schedule: | ||||||
|     - package-ecosystem: 'cargo' # See documentation for possible values |       interval: weekly | ||||||
|       directory: '/src/wasm-lib/' # Location of package manifests |       day: monday | ||||||
|       schedule: |     reviewers: | ||||||
|           interval: 'weekly' |       - adamchalmers | ||||||
|       reviewers: |       - jessfraz | ||||||
|           - adamchalmers |   - package-ecosystem: 'cargo' # See documentation for possible values | ||||||
|           - jessfraz |     directory: '/src/wasm-lib/' # Location of package manifests | ||||||
|  |     schedule: | ||||||
|  |       interval: weekly | ||||||
|  |       day: monday | ||||||
|  |     reviewers: | ||||||
|  |       - adamchalmers | ||||||
|  |       - jessfraz | ||||||
|  |     groups: | ||||||
|  |       serde-dependencies: | ||||||
|  |         patterns: | ||||||
|  |           - "serde*" | ||||||
|  |       wasm-bindgen-deps: | ||||||
|  |         patterns: | ||||||
|  |           - "wasm-bindgen*" | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/build-and-store-wasm.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -27,7 +27,7 @@ jobs: | |||||||
|  |  | ||||||
|  |  | ||||||
|       # Upload the WASM bundle as an artifact |       # Upload the WASM bundle as an artifact | ||||||
|       - uses: actions/upload-artifact@v3 |       - uses: actions/upload-artifact@v4 | ||||||
|         with: |         with: | ||||||
|           name: wasm-bundle |           name: wasm-bundle | ||||||
|           path: src/wasm-lib/pkg |           path: src/wasm-lib/pkg | ||||||
|  | |||||||
							
								
								
									
										24
									
								
								.github/workflows/build-apps.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -126,7 +126,13 @@ jobs: | |||||||
|           node-version-file: '.nvmrc' |           node-version-file: '.nvmrc' | ||||||
|           cache: 'yarn' # Set this to npm, yarn or pnpm. |           cache: 'yarn' # Set this to npm, yarn or pnpm. | ||||||
|  |  | ||||||
|       - run: yarn install |       - name: yarn install | ||||||
|  |         # Windows is picky sometimes and fails on fetch. Step takes about ~30s | ||||||
|  |         uses: nick-fields/retry@v3.0.0 | ||||||
|  |         with: | ||||||
|  |           timeout_minutes: 2 | ||||||
|  |           max_attempts: 3 | ||||||
|  |           command: yarn install | ||||||
|  |  | ||||||
|       - run: yarn tronb:vite |       - run: yarn tronb:vite | ||||||
|  |  | ||||||
| @ -173,7 +179,13 @@ jobs: | |||||||
|           CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} |           CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | ||||||
|           CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} |           CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||||||
|           WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} |           WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} | ||||||
|         run: yarn electron-builder --config --publish always |           DEBUG: "electron-notarize*" | ||||||
|  |         # TODO: Fix electron-notarize flakes. The logs above should help gather more data on failures | ||||||
|  |         uses: nick-fields/retry@v3.0.0 | ||||||
|  |         with: | ||||||
|  |           timeout_minutes: 10 | ||||||
|  |           max_attempts: 3 | ||||||
|  |           command: yarn electron-builder --config --publish always | ||||||
|  |  | ||||||
|       - name: List artifacts in out/ |       - name: List artifacts in out/ | ||||||
|         run: ls -R out |         run: ls -R out | ||||||
| @ -228,7 +240,13 @@ jobs: | |||||||
|           CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} |           CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | ||||||
|           CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} |           CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||||||
|           WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} |           WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} | ||||||
|         run: yarn electron-builder --config --publish always |           DEBUG: "electron-notarize*" | ||||||
|  |         # TODO: Fix electron-notarize flakes. The logs above should help gather more data on failures | ||||||
|  |         uses: nick-fields/retry@v3.0.0 | ||||||
|  |         with: | ||||||
|  |           timeout_minutes: 10 | ||||||
|  |           max_attempts: 3 | ||||||
|  |           command: yarn electron-builder --config --publish always | ||||||
|  |  | ||||||
|       - uses: actions/upload-artifact@v4 |       - uses: actions/upload-artifact@v4 | ||||||
|         if: ${{ env.IS_RELEASE == 'true' }} |         if: ${{ env.IS_RELEASE == 'true' }} | ||||||
|  | |||||||
							
								
								
									
										44
									
								
								.github/workflows/cargo-bench.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -1,44 +0,0 @@ | |||||||
| on: |  | ||||||
|   push: |  | ||||||
|     branches: |  | ||||||
|       - main |  | ||||||
|     paths: |  | ||||||
|       - '**.rs' |  | ||||||
|       - '**/Cargo.toml' |  | ||||||
|       - '**/Cargo.lock' |  | ||||||
|       - '**/rust-toolchain.toml' |  | ||||||
|       - .github/workflows/cargo-bench.yml |  | ||||||
|   pull_request: |  | ||||||
|     paths: |  | ||||||
|       - '**.rs' |  | ||||||
|       - '**/Cargo.toml' |  | ||||||
|       - '**/Cargo.lock' |  | ||||||
|       - '**/rust-toolchain.toml' |  | ||||||
|       - .github/workflows/cargo-bench.yml |  | ||||||
|   workflow_dispatch: |  | ||||||
| permissions: read-all |  | ||||||
| concurrency: |  | ||||||
|   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} |  | ||||||
|   cancel-in-progress: true |  | ||||||
| name: cargo bench |  | ||||||
| jobs: |  | ||||||
|   cargo-bench: |  | ||||||
|     name: Benchmark with iai |  | ||||||
|     runs-on: ubuntu-latest-8-cores |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - uses: dtolnay/rust-toolchain@stable |  | ||||||
|       - name: Install dependencies |  | ||||||
|         run: | |  | ||||||
|           cargo install cargo-criterion |  | ||||||
|           sudo apt update |  | ||||||
|           sudo apt install -y valgrind |  | ||||||
|       - name: Rust Cache |  | ||||||
|         uses: Swatinem/rust-cache@v2.6.1 |  | ||||||
|       - name: Benchmark kcl library |  | ||||||
|         shell: bash |  | ||||||
|         run: |- |  | ||||||
|           cd src/wasm-lib/kcl; cargo bench --all-features -- iai |  | ||||||
|         env: |  | ||||||
|           KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}} |  | ||||||
|  |  | ||||||
							
								
								
									
										32
									
								
								.github/workflows/codemirror-lang-kcl.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,32 @@ | |||||||
|  | name: CodeMirror Lang KCL | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |  | ||||||
|  | concurrency: | ||||||
|  |   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | ||||||
|  |   cancel-in-progress: true | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   yarn-unit-test: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v4 | ||||||
|  |  | ||||||
|  |       - uses: actions/setup-node@v4 | ||||||
|  |         with: | ||||||
|  |           node-version-file: '.nvmrc' | ||||||
|  |           cache: 'yarn' | ||||||
|  |  | ||||||
|  |       - run: yarn install | ||||||
|  |         working-directory: packages/codemirror-lang-kcl | ||||||
|  |  | ||||||
|  |       - run: yarn tsc | ||||||
|  |         working-directory: packages/codemirror-lang-kcl | ||||||
|  |  | ||||||
|  |       - name: run unit tests | ||||||
|  |         run: yarn test | ||||||
|  |         working-directory: packages/codemirror-lang-kcl | ||||||
							
								
								
									
										166
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -33,20 +33,19 @@ jobs: | |||||||
|             rust: |             rust: | ||||||
|               - 'src/wasm-lib/**' |               - 'src/wasm-lib/**' | ||||||
|  |  | ||||||
|   browser: |   electron: | ||||||
|     timeout-minutes: ${{ matrix.os == 'macos-14' && 60 || 50 }} |     timeout-minutes: 60 | ||||||
|     name: playwright:browser:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} |     name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} | ||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       matrix: |       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] |         shardIndex: [1, 2, 3, 4] | ||||||
|         shardTotal: [4] |         shardTotal: [4] | ||||||
|     runs-on: ${{ matrix.os }} |     runs-on: ${{ matrix.os }} | ||||||
|     needs: check-rust-changes |     needs: check-rust-changes | ||||||
|     steps: |     steps: | ||||||
|     - name: Tune GitHub-hosted runner network |  | ||||||
|       uses: smorimoto/tune-github-hosted-runner-network@v1 |  | ||||||
|     - uses: actions/checkout@v4 |     - uses: actions/checkout@v4 | ||||||
|     - uses: actions/setup-node@v4 |     - uses: actions/setup-node@v4 | ||||||
|       with: |       with: | ||||||
| @ -101,7 +100,8 @@ jobs: | |||||||
|         echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH |         echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH | ||||||
|     - name: Install vector |     - name: Install vector | ||||||
|       shell: bash |       shell: bash | ||||||
|       if:  ${{ !startsWith(matrix.os, 'windows') }} |       # TODO: figure out what to do with this, it's failing | ||||||
|  |       if: false | ||||||
|       run: | |       run: | | ||||||
|         curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh |         curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh | ||||||
|         chmod +x /tmp/vector.sh |         chmod +x /tmp/vector.sh | ||||||
| @ -123,13 +123,16 @@ jobs: | |||||||
|       if: steps.download-wasm.outcome == 'failure' |       if: steps.download-wasm.outcome == 'failure' | ||||||
|       shell: bash |       shell: bash | ||||||
|       run: yarn build:wasm |       run: yarn build:wasm | ||||||
|     - name: build web |     - name: build electron | ||||||
|       run: yarn build:local |  | ||||||
|       shell: bash |       shell: bash | ||||||
|  |       run: yarn tron:package | ||||||
|     - name: Run ubuntu/chrome snapshots |     - name: Run ubuntu/chrome snapshots | ||||||
|  |       if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }} | ||||||
|       shell: bash |       shell: bash | ||||||
|  |       # TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest, | ||||||
|  |       # but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes. | ||||||
|       run: | |       run: | | ||||||
|         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=1/1 | ||||||
|       env: |       env: | ||||||
|         CI: true |         CI: true | ||||||
|         NODE_ENV: development |         NODE_ENV: development | ||||||
| @ -150,6 +153,7 @@ jobs: | |||||||
|       continue-on-error: true |       continue-on-error: true | ||||||
|       run: rm -r test-results |       run: rm -r test-results | ||||||
|     - name: check for changes |     - name: check for changes | ||||||
|  |       if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }} | ||||||
|       shell: bash |       shell: bash | ||||||
|       id: git-check |       id: git-check | ||||||
|       run: | |       run: | | ||||||
| @ -186,12 +190,12 @@ jobs: | |||||||
|       with: |       with: | ||||||
|         name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} |         name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} | ||||||
|         path: test-results/ |         path: test-results/ | ||||||
|     - name: Run playwright/chrome flow (with retries) |     - name: Run playwright/electron flow (with retries) | ||||||
|       id: retry |       id: retry | ||||||
|       if: ${{ !cancelled() && (success() || failure()) }} |       if: ${{ !cancelled() && (success() || failure()) }} | ||||||
|       shell: bash |       shell: bash | ||||||
|       run: | |       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: |       env: | ||||||
|         CI: true |         CI: true | ||||||
|         FAIL_ON_CONSOLE_ERRORS: true |         FAIL_ON_CONSOLE_ERRORS: true | ||||||
| @ -199,11 +203,6 @@ jobs: | |||||||
|         VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} |         VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} | ||||||
|         VITE_KC_SKIP_AUTH: true |         VITE_KC_SKIP_AUTH: true | ||||||
|         token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} |         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 |     - uses: actions/upload-artifact@v4 | ||||||
|       if: always() |       if: always() | ||||||
|       with: |       with: | ||||||
| @ -221,136 +220,3 @@ jobs: | |||||||
|         retention-days: 30 |         retention-days: 30 | ||||||
|         overwrite: true |         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@v7 |  | ||||||
|       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 |  | ||||||
|  | |||||||
							
								
								
									
										57
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @ -337,13 +337,47 @@ For individual testing: | |||||||
| yarn test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false | yarn test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default. | Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro) tests, in interactive mode by default. | ||||||
|  |  | ||||||
| ### Rust tests | ### Rust tests | ||||||
|  |  | ||||||
| ```bash | **Dependencies** | ||||||
|  |  | ||||||
|  | - `KITTYCAD_API_TOKEN` | ||||||
|  | - `cargo-nextest` | ||||||
|  | - `just` | ||||||
|  |  | ||||||
|  | #### Setting KITTYCAD_API_TOKEN | ||||||
|  | Use the production zoo.dev token, set this environment variable before running the tests | ||||||
|  |  | ||||||
|  | #### Installing cargonextest | ||||||
|  |  | ||||||
|  | ``` | ||||||
| cd src/wasm-lib | cd src/wasm-lib | ||||||
| KITTYCAD_API_TOKEN=XXX cargo test -- --test-threads=1 | cargo search cargo-nextest | ||||||
|  | cargo install cargo-nextest | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### just | ||||||
|  | install [`just`](https://github.com/casey/just?tab=readme-ov-file#pre-built-binaries) | ||||||
|  |  | ||||||
|  | #### Running the tests | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # With just | ||||||
|  | # Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set | ||||||
|  | # Make sure you installed cargo-nextest | ||||||
|  | # Make sure you installed just | ||||||
|  | cd src/wasm-lib | ||||||
|  | just test | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Without just | ||||||
|  | # Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set | ||||||
|  | # Make sure you installed cargo-nextest | ||||||
|  | cd src/wasm-lib | ||||||
|  | export RUST_BRACKTRACE="full" && cargo nextest run --workspace --test-threads=1 | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Where `XXX` is an API token from the production engine (NOT the dev environment). | Where `XXX` is an API token from the production engine (NOT the dev environment). | ||||||
| @ -388,23 +422,6 @@ yarn test:unit:local | |||||||
|  |  | ||||||
| #### E2E Tests | #### 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** | **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. | 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. | ||||||
|  | |||||||
							
								
								
									
										42
									
								
								docs/kcl/circleThreePoint.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										43
									
								
								docs/kcl/helixRevolutions.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -35,6 +35,7 @@ layout: manual | |||||||
| * [`ceil`](kcl/ceil) | * [`ceil`](kcl/ceil) | ||||||
| * [`chamfer`](kcl/chamfer) | * [`chamfer`](kcl/chamfer) | ||||||
| * [`circle`](kcl/circle) | * [`circle`](kcl/circle) | ||||||
|  | * [`circleThreePoint`](kcl/circleThreePoint) | ||||||
| * [`close`](kcl/close) | * [`close`](kcl/close) | ||||||
| * [`cm`](kcl/cm) | * [`cm`](kcl/cm) | ||||||
| * [`cos`](kcl/cos) | * [`cos`](kcl/cos) | ||||||
| @ -47,11 +48,11 @@ layout: manual | |||||||
| * [`getOppositeEdge`](kcl/getOppositeEdge) | * [`getOppositeEdge`](kcl/getOppositeEdge) | ||||||
| * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) | * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) | ||||||
| * [`helix`](kcl/helix) | * [`helix`](kcl/helix) | ||||||
|  | * [`helixRevolutions`](kcl/helixRevolutions) | ||||||
| * [`hole`](kcl/hole) | * [`hole`](kcl/hole) | ||||||
| * [`hollow`](kcl/hollow) | * [`hollow`](kcl/hollow) | ||||||
| * [`import`](kcl/import) | * [`import`](kcl/import) | ||||||
| * [`inch`](kcl/inch) | * [`inch`](kcl/inch) | ||||||
| * [`int`](kcl/int) |  | ||||||
| * [`lastSegX`](kcl/lastSegX) | * [`lastSegX`](kcl/lastSegX) | ||||||
| * [`lastSegY`](kcl/lastSegY) | * [`lastSegY`](kcl/lastSegY) | ||||||
| * [`legAngX`](kcl/legAngX) | * [`legAngX`](kcl/legAngX) | ||||||
| @ -80,6 +81,7 @@ layout: manual | |||||||
| * [`pi`](kcl/pi) | * [`pi`](kcl/pi) | ||||||
| * [`polar`](kcl/polar) | * [`polar`](kcl/polar) | ||||||
| * [`polygon`](kcl/polygon) | * [`polygon`](kcl/polygon) | ||||||
|  | * [`pop`](kcl/pop) | ||||||
| * [`pow`](kcl/pow) | * [`pow`](kcl/pow) | ||||||
| * [`profileStart`](kcl/profileStart) | * [`profileStart`](kcl/profileStart) | ||||||
| * [`profileStartX`](kcl/profileStartX) | * [`profileStartX`](kcl/profileStartX) | ||||||
| @ -101,7 +103,6 @@ layout: manual | |||||||
| * [`sin`](kcl/sin) | * [`sin`](kcl/sin) | ||||||
| * [`sqrt`](kcl/sqrt) | * [`sqrt`](kcl/sqrt) | ||||||
| * [`startProfileAt`](kcl/startProfileAt) | * [`startProfileAt`](kcl/startProfileAt) | ||||||
| * [`startSketchAt`](kcl/startSketchAt) |  | ||||||
| * [`startSketchOn`](kcl/startSketchOn) | * [`startSketchOn`](kcl/startSketchOn) | ||||||
| * [`sweep`](kcl/sweep) | * [`sweep`](kcl/sweep) | ||||||
| * [`tan`](kcl/tan) | * [`tan`](kcl/tan) | ||||||
|  | |||||||
| @ -4,6 +4,8 @@ excerpt: "Convert a number to an integer." | |||||||
| layout: manual | layout: manual | ||||||
| --- | --- | ||||||
|  |  | ||||||
|  | **WARNING:** This function is deprecated. | ||||||
|  |  | ||||||
| Convert a number to an integer. | Convert a number to an integer. | ||||||
|  |  | ||||||
| DEPRECATED use floor(), ceil(), or round(). | DEPRECATED use floor(), ceil(), or round(). | ||||||
|  | |||||||
| @ -95,7 +95,8 @@ fn cube(length, center) { | |||||||
|   p2 = [l + x, l + y] |   p2 = [l + x, l + y] | ||||||
|   p3 = [l + x, -l + y] |   p3 = [l + x, -l + y] | ||||||
|  |  | ||||||
|   return startSketchAt(p0) |   return startSketchOn('XY') | ||||||
|  |     |> startProfileAt(p0, %) | ||||||
|     |> lineTo(p1, %) |     |> lineTo(p1, %) | ||||||
|     |> lineTo(p2, %) |     |> lineTo(p2, %) | ||||||
|     |> lineTo(p3, %) |     |> lineTo(p3, %) | ||||||
| @ -132,7 +133,8 @@ fn cube(length, center) { | |||||||
|   p2 = [l + x, l + y] |   p2 = [l + x, l + y] | ||||||
|   p3 = [l + x, -l + y] |   p3 = [l + x, -l + y] | ||||||
|  |  | ||||||
|   return startSketchAt(p0) |   return startSketchOn('XY') | ||||||
|  |     |> startProfileAt(p0, %) | ||||||
|     |> lineTo(p1, %) |     |> lineTo(p1, %) | ||||||
|     |> lineTo(p2, %) |     |> lineTo(p2, %) | ||||||
|     |> lineTo(p3, %) |     |> lineTo(p3, %) | ||||||
| @ -195,7 +197,8 @@ fn transform(i) { | |||||||
|     { rotation = { angle = 45 * i } } |     { rotation = { angle = 45 * i } } | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
| startSketchAt([0, 0]) | startSketchOn('XY') | ||||||
|  |   |> startProfileAt([0, 0], %) | ||||||
|   |> polygon({ |   |> polygon({ | ||||||
|        radius = 10, |        radius = 10, | ||||||
|        numSides = 4, |        numSides = 4, | ||||||
|  | |||||||
							
								
								
									
										39
									
								
								docs/kcl/pop.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -79,10 +79,11 @@ fn decagon(radius) { | |||||||
|   stepAngle = 1 / 10 * tau() |   stepAngle = 1 / 10 * tau() | ||||||
|  |  | ||||||
|   // Start the decagon sketch at this point. |   // 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. |     // Use a `reduce` to draw the remaining decagon sides. | ||||||
|   // For each number in the array 1..10, run the given function, |     // 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. |   // which takes a partially-sketched decagon and adds one more edge to it. | ||||||
|   fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) { |   fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) { | ||||||
|     // Draw one edge of the decagon. |     // Draw one edge of the decagon. | ||||||
| @ -97,7 +98,8 @@ fn decagon(radius) { | |||||||
| /* The `decagon` above is basically like this pseudo-code: | /* The `decagon` above is basically like this pseudo-code: | ||||||
| fn decagon(radius): | fn decagon(radius): | ||||||
|     stepAngle = (1/10) * tau() |     stepAngle = (1/10) * tau() | ||||||
|     startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)]) |     plane = startSketchOn('XY') | ||||||
|  |     startOfDecagonSketch = startProfileAt([(cos(0)*radius), (sin(0) * radius)], plane) | ||||||
|  |  | ||||||
|     // Here's the reduce part. |     // Here's the reduce part. | ||||||
|     partialDecagon = startOfDecagonSketch |     partialDecagon = startOfDecagonSketch | ||||||
|  | |||||||
| @ -28,7 +28,8 @@ segEnd(tag: TagIdentifier) -> [number] | |||||||
|  |  | ||||||
| ```js | ```js | ||||||
| w = 15 | w = 15 | ||||||
| cube = startSketchAt([0, 0]) | cube = startSketchOn('XY') | ||||||
|  |   |> startProfileAt([0, 0], %) | ||||||
|   |> line([w, 0], %, $line1) |   |> line([w, 0], %, $line1) | ||||||
|   |> line([0, w], %, $line2) |   |> line([0, w], %, $line2) | ||||||
|   |> line([-w, 0], %, $line3) |   |> line([-w, 0], %, $line3) | ||||||
| @ -37,7 +38,8 @@ cube = startSketchAt([0, 0]) | |||||||
|   |> extrude(5, %) |   |> extrude(5, %) | ||||||
|  |  | ||||||
| fn cylinder(radius, tag) { | fn cylinder(radius, tag) { | ||||||
|   return startSketchAt([0, 0]) |   return startSketchOn('XY') | ||||||
|  |     |> startProfileAt([0, 0], %) | ||||||
|     |> circle({ |     |> circle({ | ||||||
|          radius = radius, |          radius = radius, | ||||||
|          center = segEnd(tag) |          center = segEnd(tag) | ||||||
|  | |||||||
| @ -28,7 +28,8 @@ segStart(tag: TagIdentifier) -> [number] | |||||||
|  |  | ||||||
| ```js | ```js | ||||||
| w = 15 | w = 15 | ||||||
| cube = startSketchAt([0, 0]) | cube = startSketchOn('XY') | ||||||
|  |   |> startProfileAt([0, 0], %) | ||||||
|   |> line([w, 0], %, $line1) |   |> line([w, 0], %, $line1) | ||||||
|   |> line([0, w], %, $line2) |   |> line([0, w], %, $line2) | ||||||
|   |> line([-w, 0], %, $line3) |   |> line([-w, 0], %, $line3) | ||||||
| @ -37,7 +38,8 @@ cube = startSketchAt([0, 0]) | |||||||
|   |> extrude(5, %) |   |> extrude(5, %) | ||||||
|  |  | ||||||
| fn cylinder(radius, tag) { | fn cylinder(radius, tag) { | ||||||
|   return startSketchAt([0, 0]) |   return startSketchOn('XY') | ||||||
|  |     |> startProfileAt([0, 0], %) | ||||||
|     |> circle({ |     |> circle({ | ||||||
|          radius = radius, |          radius = radius, | ||||||
|          center = segStart(tag) |          center = segStart(tag) | ||||||
|  | |||||||
| @ -4,6 +4,8 @@ excerpt: "Start a new 2-dimensional sketch at a given point on the 'XY' plane." | |||||||
| layout: manual | layout: manual | ||||||
| --- | --- | ||||||
|  |  | ||||||
|  | **WARNING:** This function is deprecated. | ||||||
|  |  | ||||||
| Start a new 2-dimensional sketch at a given point on the 'XY' plane. | Start a new 2-dimensional sketch at a given point on the 'XY' plane. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										29585
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
							
						
						| @ -1,19 +1,19 @@ | |||||||
| --- | --- | ||||||
| title: "AxisOrEdgeReference" | title: "Axis2dOrEdgeReference" | ||||||
| excerpt: "Axis or tagged edge." | excerpt: "A 2D axis or tagged edge." | ||||||
| layout: manual | layout: manual | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
| Axis or tagged edge. | A 2D axis or tagged edge. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| **This schema accepts any of the following:** | **This schema accepts any of the following:** | ||||||
| 
 | 
 | ||||||
| Axis and origin. | 2D axis and origin. | ||||||
| 
 | 
 | ||||||
| [`AxisAndOrigin`](/docs/kcl/types/AxisAndOrigin) | [`AxisAndOrigin2d`](/docs/kcl/types/AxisAndOrigin2d) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
							
								
								
									
										42
									
								
								docs/kcl/types/Axis3dOrEdgeReference.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,42 @@ | |||||||
|  | --- | ||||||
|  | title: "Axis3dOrEdgeReference" | ||||||
|  | excerpt: "A 3D axis or tagged edge." | ||||||
|  | layout: manual | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | A 3D axis or tagged edge. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **This schema accepts any of the following:** | ||||||
|  |  | ||||||
|  | 3D axis and origin. | ||||||
|  |  | ||||||
|  | [`AxisAndOrigin3d`](/docs/kcl/types/AxisAndOrigin3d) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  | Tagged edge. | ||||||
|  |  | ||||||
|  | [`EdgeReference`](/docs/kcl/types/EdgeReference) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -1,10 +1,10 @@ | |||||||
| --- | --- | ||||||
| title: "AxisAndOrigin" | title: "AxisAndOrigin2d" | ||||||
| excerpt: "Axis and origin." | excerpt: "A 2D axis and origin." | ||||||
| layout: manual | layout: manual | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
| Axis and origin. | A 2D axis and origin. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
							
								
								
									
										105
									
								
								docs/kcl/types/AxisAndOrigin3d.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,105 @@ | |||||||
|  | --- | ||||||
|  | title: "AxisAndOrigin3d" | ||||||
|  | excerpt: "A 3D axis and origin." | ||||||
|  | layout: manual | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | A 3D axis and origin. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **This schema accepts exactly one of the following:** | ||||||
|  |  | ||||||
|  | X-axis. | ||||||
|  |  | ||||||
|  | **enum:** `X` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  | Y-axis. | ||||||
|  |  | ||||||
|  | **enum:** `Y` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  | Z-axis. | ||||||
|  |  | ||||||
|  | **enum:** `Z` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  | Flip the X-axis. | ||||||
|  |  | ||||||
|  | **enum:** `-X` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  | Flip the Y-axis. | ||||||
|  |  | ||||||
|  | **enum:** `-Y` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  | Flip the Z-axis. | ||||||
|  |  | ||||||
|  | **enum:** `-Z` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `custom` |`object`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										23
									
								
								docs/kcl/types/CircleThreePointData.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,23 @@ | |||||||
|  | --- | ||||||
|  | title: "CircleThreePointData" | ||||||
|  | excerpt: "Data for drawing a 3-point circle" | ||||||
|  | layout: manual | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | Data for drawing a 3-point circle | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `p1` |`[number, number]`| Point one for circle derivation. | No | | ||||||
|  | | `p2` |`[number, number]`| Point two for circle derivation. | No | | ||||||
|  | | `p3` |`[number, number]`| Point three for circle derivation. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								docs/kcl/types/Face.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,28 @@ | |||||||
|  | --- | ||||||
|  | title: "Face" | ||||||
|  | excerpt: "A face." | ||||||
|  | layout: manual | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | A face. | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `id` |`string`| The id of the face. | No | | ||||||
|  | | `value` |`string`| The tag of the face. | No | | ||||||
|  | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No | | ||||||
|  | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | | ||||||
|  | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||||
|  | | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A face. | No | | ||||||
|  | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										26
									
								
								docs/kcl/types/Helix.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,26 @@ | |||||||
|  | --- | ||||||
|  | title: "Helix" | ||||||
|  | excerpt: "A helix." | ||||||
|  | layout: manual | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | A helix. | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `value` |`string`| The id of the helix. | No | | ||||||
|  | | `revolutions` |`number`| Number of revolutions. | No | | ||||||
|  | | `angleStart` |`number`| Start angle (in degrees). | No | | ||||||
|  | | `ccw` |`boolean`| Is the helix rotation counter clockwise? | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No | | ||||||
|  | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -1,10 +1,10 @@ | |||||||
| --- | --- | ||||||
| title: "HelixData" | title: "HelixData" | ||||||
| excerpt: "Data for helices." | excerpt: "Data for a helix." | ||||||
| layout: manual | layout: manual | ||||||
| --- | --- | ||||||
|  |  | ||||||
| Data for helices. | Data for a helix. | ||||||
|  |  | ||||||
| **Type:** `object` | **Type:** `object` | ||||||
|  |  | ||||||
| @ -19,6 +19,8 @@ Data for helices. | |||||||
| | `revolutions` |`number`| Number of revolutions. | No | | | `revolutions` |`number`| Number of revolutions. | No | | ||||||
| | `angleStart` |`number`| Start angle (in degrees). | No | | | `angleStart` |`number`| Start angle (in degrees). | No | | ||||||
| | `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No | | | `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No | | ||||||
| | `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No | | | `length` |`number`| Length of the helix. This is not necessary if the helix is created around an edge. If not given the length of the edge is used. | No | | ||||||
|  | | `radius` |`number`| Radius of the helix. | No | | ||||||
|  | | `axis` |[`Axis3dOrEdgeReference`](/docs/kcl/types/Axis3dOrEdgeReference)| Axis to use as mirror. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										24
									
								
								docs/kcl/types/HelixRevolutionsData.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,24 @@ | |||||||
|  | --- | ||||||
|  | title: "HelixRevolutionsData" | ||||||
|  | excerpt: "Data for helix revolutions." | ||||||
|  | layout: manual | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | Data for helix revolutions. | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `revolutions` |`number`| Number of revolutions. | No | | ||||||
|  | | `angleStart` |`number`| Start angle (in degrees). | No | | ||||||
|  | | `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No | | ||||||
|  | | `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										26
									
								
								docs/kcl/types/HelixValue.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,26 @@ | |||||||
|  | --- | ||||||
|  | title: "HelixValue" | ||||||
|  | excerpt: "A helix." | ||||||
|  | layout: manual | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | A helix. | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `value` |`string`| The id of the helix. | No | | ||||||
|  | | `revolutions` |`number`| Number of revolutions. | No | | ||||||
|  | | `angleStart` |`number`| Start angle (in degrees). | No | | ||||||
|  | | `ccw` |`boolean`| Is the helix rotation counter clockwise? | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No | | ||||||
|  | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -168,7 +168,6 @@ Any KCL value. | |||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| A plane. |  | ||||||
|  |  | ||||||
| **Type:** `object` | **Type:** `object` | ||||||
|  |  | ||||||
| @ -181,17 +180,10 @@ A plane. | |||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `type` |enum: [`Plane`](/docs/kcl/types/Plane)|  | No | | | `type` |enum: [`Plane`](/docs/kcl/types/Plane)|  | No | | ||||||
| | `id` |`string`| The id of the plane. | No | | | `value` |[`Plane`](/docs/kcl/types/Plane)| Any KCL value. | No | | ||||||
| | `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No | |  | ||||||
| | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | |  | ||||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | |  | ||||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | |  | ||||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | |  | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| A face. |  | ||||||
|  |  | ||||||
| **Type:** `object` | **Type:** `object` | ||||||
|  |  | ||||||
| @ -203,14 +195,8 @@ A face. | |||||||
|  |  | ||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `type` |enum: `Face`|  | No | | | `type` |enum: [`Face`](/docs/kcl/types/Face)|  | No | | ||||||
| | `id` |`string`| The id of the face. | No | | | `value` |[`Face`](/docs/kcl/types/Face)| Any KCL value. | No | | ||||||
| | `value` |`string`| The tag of the face. | No | |  | ||||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No | |  | ||||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | |  | ||||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | |  | ||||||
| | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | |  | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| @ -246,7 +232,6 @@ A face. | |||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| An solid is a collection of extrude surfaces. |  | ||||||
|  |  | ||||||
| **Type:** `object` | **Type:** `object` | ||||||
|  |  | ||||||
| @ -259,14 +244,7 @@ An solid is a collection of extrude surfaces. | |||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `type` |enum: [`Solid`](/docs/kcl/types/Solid)|  | No | | | `type` |enum: [`Solid`](/docs/kcl/types/Solid)|  | No | | ||||||
| | `id` |`string`| The id of the solid. | No | | | `value` |[`Solid`](/docs/kcl/types/Solid)| Any KCL value. | No | | ||||||
| | `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No | |  | ||||||
| | `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No | |  | ||||||
| | `height` |`number`| The height of the solid. | No | |  | ||||||
| | `startCapId` |`string`| The id of the extrusion start cap | No | |  | ||||||
| | `endCapId` |`string`| The id of the extrusion end cap | No | |  | ||||||
| | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | |  | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| @ -285,6 +263,22 @@ An solid is a collection of extrude surfaces. | |||||||
| | `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`|  | No | | | `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: [`Helix`](/docs/kcl/types/Helix)|  | No | | ||||||
|  | | `value` |[`Helix`](/docs/kcl/types/Helix)| Any KCL value. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| Data for an imported geometry. | Data for an imported geometry. | ||||||
|  |  | ||||||
|  | |||||||
| @ -16,6 +16,6 @@ Data for a mirror. | |||||||
|  |  | ||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis to use as mirror. | No | | | `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis to use as mirror. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ A plane. | |||||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | ||||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | ||||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A plane. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ Data for revolution surfaces. | |||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `angle` |`number` (**maximum:** 360.0) (**minimum:** -360.0)| Angle to revolve (in degrees). Default is 360. | No | | | `angle` |`number` (**maximum:** 360.0) (**minimum:** -360.0)| Angle to revolve (in degrees). Default is 360. | No | | ||||||
| | `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis of revolution. | No | | | `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis of revolution. | No | | ||||||
| | `tolerance` |`number`| Tolerance for the revolve operation. | No | | | `tolerance` |`number`| Tolerance for the revolve operation. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ A sketch is a collection of paths. | |||||||
| | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | ||||||
| | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | ||||||
| | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -30,6 +30,7 @@ A sketch is a collection of paths. | |||||||
| | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | | `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No | | ||||||
| | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | ||||||
| | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | | `tags` |`object`| Tag identifiers that have been declared in this sketch. | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ A plane. | |||||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | ||||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | | ||||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -54,6 +55,7 @@ A face. | |||||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No | | ||||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||||
| | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ An solid is a collection of extrude surfaces. | |||||||
| | `startCapId` |`string`| The id of the extrusion start cap | No | | | `startCapId` |`string`| The id of the extrusion start cap | No | | ||||||
| | `endCapId` |`string`| The id of the extrusion end cap | No | | | `endCapId` |`string`| The id of the extrusion end cap | No | | ||||||
| | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| An solid is a collection of extrude surfaces. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -32,6 +32,7 @@ An solid is a collection of extrude surfaces. | |||||||
| | `startCapId` |`string`| The id of the extrusion start cap | No | | | `startCapId` |`string`| The id of the extrusion start cap | No | | ||||||
| | `endCapId` |`string`| The id of the extrusion end cap | No | | | `endCapId` |`string`| The id of the extrusion end cap | No | | ||||||
| | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | | `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No | | ||||||
|  | | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A solid or a group of solids. | No | | ||||||
| | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ Data for a sweep. | |||||||
|  |  | ||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No | | | `path` |[`SweepPath`](/docs/kcl/types/SweepPath)| The path to sweep along. | No | | ||||||
| | `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No | | | `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No | | ||||||
| | `tolerance` |`number`| Tolerance for the sweep operation. | No | | | `tolerance` |`number`| Tolerance for the sweep operation. | No | | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										42
									
								
								docs/kcl/types/SweepPath.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,42 @@ | |||||||
|  | --- | ||||||
|  | title: "SweepPath" | ||||||
|  | excerpt: "A path to sweep along." | ||||||
|  | layout: manual | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | A path to sweep along. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **This schema accepts any of the following:** | ||||||
|  |  | ||||||
|  | A path to sweep along. | ||||||
|  |  | ||||||
|  | [`Sketch`](/docs/kcl/types/Sketch) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  | A path to sweep along. | ||||||
|  |  | ||||||
|  | [`Helix`](/docs/kcl/types/Helix) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										107
									
								
								docs/kcl/types/UnitLen.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,107 @@ | |||||||
|  | --- | ||||||
|  | title: "UnitLen" | ||||||
|  | excerpt: "" | ||||||
|  | layout: manual | ||||||
|  | --- | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **This schema accepts exactly one of the following:** | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: `Mm`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: `Cm`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: `M`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: `Inches`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: `Feet`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `type` |enum: `Yards`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -1,22 +1,11 @@ | |||||||
| import { test, expect } from '@playwright/test' | import { test, expect } from './zoo-test' | ||||||
|  |  | ||||||
| import { setupElectron, tearDown } from './test-utils' |  | ||||||
|  |  | ||||||
| test.afterEach(async ({ page }, testInfo) => { |  | ||||||
|   await tearDown(page, testInfo) |  | ||||||
| }) |  | ||||||
|  |  | ||||||
| test.describe('Electron app header tests', () => { | test.describe('Electron app header tests', () => { | ||||||
|   test( |   test( | ||||||
|     'Open Command Palette button has correct shortcut', |     'Open Command Palette button has correct shortcut', | ||||||
|     { tag: '@electron' }, |     { tag: '@electron' }, | ||||||
|     async ({ browserName }, testInfo) => { |     async ({ page }, testInfo) => { | ||||||
|       const { electronApp, page } = await setupElectron({ |       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|         testInfo, |  | ||||||
|         folderSetupFn: async () => {}, |  | ||||||
|       }) |  | ||||||
|  |  | ||||||
|       await page.setViewportSize({ width: 1200, height: 500 }) |  | ||||||
|  |  | ||||||
|       // No space before the shortcut since it checks textContent. |       // No space before the shortcut since it checks textContent. | ||||||
|       let text |       let text | ||||||
| @ -34,21 +23,14 @@ test.describe('Electron app header tests', () => { | |||||||
|       const commandsButton = page.getByRole('button', { name: 'Commands' }) |       const commandsButton = page.getByRole('button', { name: 'Commands' }) | ||||||
|       await expect(commandsButton).toBeVisible() |       await expect(commandsButton).toBeVisible() | ||||||
|       await expect(commandsButton).toHaveText(text) |       await expect(commandsButton).toHaveText(text) | ||||||
|  |  | ||||||
|       await electronApp.close() |  | ||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|   test( |   test( | ||||||
|     'User settings has correct shortcut', |     'User settings has correct shortcut', | ||||||
|     { tag: '@electron' }, |     { tag: '@electron' }, | ||||||
|     async ({ browserName }, testInfo) => { |     async ({ page }, testInfo) => { | ||||||
|       const { electronApp, page } = await setupElectron({ |       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|         testInfo, |  | ||||||
|         folderSetupFn: async () => {}, |  | ||||||
|       }) |  | ||||||
|  |  | ||||||
|       await page.setViewportSize({ width: 1200, height: 500 }) |  | ||||||
|  |  | ||||||
|       // Open the user sidebar menu. |       // Open the user sidebar menu. | ||||||
|       await page.getByTestId('user-sidebar-toggle').click() |       await page.getByTestId('user-sidebar-toggle').click() | ||||||
| @ -59,8 +41,6 @@ test.describe('Electron app header tests', () => { | |||||||
|       const userSettingsButton = page.getByTestId('user-settings') |       const userSettingsButton = page.getByTestId('user-settings') | ||||||
|       await expect(userSettingsButton).toBeVisible() |       await expect(userSettingsButton).toBeVisible() | ||||||
|       await expect(userSettingsButton).toHaveText(text) |       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 { | import { | ||||||
|   getUtils, |   getUtils, | ||||||
|   TEST_COLORS, |   TEST_COLORS, | ||||||
|   setup, |  | ||||||
|   tearDown, |  | ||||||
|   commonPoints, |   commonPoints, | ||||||
|   PERSIST_MODELING_CONTEXT, |   PERSIST_MODELING_CONTEXT, | ||||||
| } from './test-utils' | } from './test-utils' | ||||||
|  | import { HomePageFixture } from './fixtures/homePageFixture' | ||||||
| test.beforeEach(async ({ context, page }, testInfo) => { |  | ||||||
|   await setup(context, page, testInfo) |  | ||||||
| }) |  | ||||||
|  |  | ||||||
| test.afterEach(async ({ page }, testInfo) => { |  | ||||||
|   await tearDown(page, testInfo) |  | ||||||
| }) |  | ||||||
|  |  | ||||||
| test.setTimeout(120000) | test.setTimeout(120000) | ||||||
|  |  | ||||||
| async function doBasicSketch(page: Page, openPanes: string[]) { | async function doBasicSketch( | ||||||
|  |   page: Page, | ||||||
|  |   homePage: HomePageFixture, | ||||||
|  |   openPanes: string[] | ||||||
|  | ) { | ||||||
|   const u = await getUtils(page) |   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 |   const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||||
|  |  | ||||||
|   await u.waitForAuthSkipAppStart() |   await homePage.goToModelingScene() | ||||||
|  |   await u.waitForPageLoad() | ||||||
|  |   await page.waitForTimeout(1000) | ||||||
|   await u.openDebugPanel() |   await u.openDebugPanel() | ||||||
|  |  | ||||||
|   // If we have the code pane open, we should see the code. |   // 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.describe('Basic sketch', () => { | ||||||
|   test('code pane open at start', { tag: ['@skipWin'] }, async ({ page }) => { |   test.fixme('code pane open at start', async ({ page, homePage }) => { | ||||||
|     // Skip on windows it is being weird. |     await doBasicSketch(page, homePage, ['code']) | ||||||
|     test.skip(process.platform === 'win32', 'Skip on windows') |  | ||||||
|     await doBasicSketch(page, ['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 |     // Load the app with the code panes | ||||||
|     await page.addInitScript(async (persistModelingContext) => { |     await page.addInitScript(async (persistModelingContext) => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
| @ -162,6 +157,6 @@ test.describe('Basic sketch', () => { | |||||||
|         JSON.stringify({ openPanes: [] }) |         JSON.stringify({ openPanes: [] }) | ||||||
|       ) |       ) | ||||||
|     }, PERSIST_MODELING_CONTEXT) |     }, PERSIST_MODELING_CONTEXT) | ||||||
|     await doBasicSketch(page, []) |     await doBasicSketch(page, homePage, []) | ||||||
|   }) |   }) | ||||||
| }) | }) | ||||||
|  | |||||||
| @ -1,27 +1,21 @@ | |||||||
| import { test, expect } from '@playwright/test' | import { test, expect, Page } from './zoo-test' | ||||||
| import { getUtils, setup, tearDown } from './test-utils' | import { HomePageFixture } from './fixtures/homePageFixture' | ||||||
|  | import { getUtils } from './test-utils' | ||||||
| import { EngineCommand } from 'lang/std/artifactGraph' | import { EngineCommand } from 'lang/std/artifactGraph' | ||||||
| import { uuidv4 } from 'lib/utils' | 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', () => { | test.describe('Can create sketches on all planes and their back sides', () => { | ||||||
|   const sketchOnPlaneAndBackSideTest = async ( |   const sketchOnPlaneAndBackSideTest = async ( | ||||||
|     page: any, |     page: Page, | ||||||
|  |     homePage: HomePageFixture, | ||||||
|     plane: string, |     plane: string, | ||||||
|     clickCoords: { x: number; y: number } |     clickCoords: { x: number; y: number } | ||||||
|   ) => { |   ) => { | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|     const PUR = 400 / 37.5 //pixeltoUnitRatio |     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() |     await u.openDebugPanel() | ||||||
|  |  | ||||||
|     const coord = |     const coord = | ||||||
| @ -83,32 +77,39 @@ test.describe('Can create sketches on all planes and their back sides', () => { | |||||||
|     await u.clearCommandLogs() |     await u.clearCommandLogs() | ||||||
|     await u.removeCurrentCode() |     await u.removeCurrentCode() | ||||||
|   } |   } | ||||||
|   test('XY', async ({ page }) => { |   test('XY', async ({ page, homePage }) => { | ||||||
|     await sketchOnPlaneAndBackSideTest( |     await sketchOnPlaneAndBackSideTest( | ||||||
|       page, |       page, | ||||||
|  |       homePage, | ||||||
|       'XY', |       'XY', | ||||||
|       { x: 600, y: 388 } // red plane |       { 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. |       // { 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 }) => { |   test('YZ', async ({ page, homePage }) => { | ||||||
|     await sketchOnPlaneAndBackSideTest(page, 'YZ', { x: 700, y: 250 }) // green plane |     await sketchOnPlaneAndBackSideTest(page, homePage, 'YZ', { x: 700, y: 250 }) // green plane | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   test('XZ', async ({ page }) => { |   test('XZ', async ({ page, homePage }) => { | ||||||
|     await sketchOnPlaneAndBackSideTest(page, '-XZ', { x: 700, y: 80 }) // blue plane |     await sketchOnPlaneAndBackSideTest(page, homePage, '-XZ', { x: 700, y: 80 }) // blue plane | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   test('-XY', async ({ page }) => { |   test('-XY', async ({ page, homePage }) => { | ||||||
|     await sketchOnPlaneAndBackSideTest(page, '-XY', { x: 600, y: 118 }) // back of red plane |     await sketchOnPlaneAndBackSideTest(page, homePage, '-XY', { | ||||||
|  |       x: 600, | ||||||
|  |       y: 118, | ||||||
|  |     }) // back of red plane | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   test('-YZ', async ({ page }) => { |   test('-YZ', async ({ page, homePage }) => { | ||||||
|     await sketchOnPlaneAndBackSideTest(page, '-YZ', { x: 700, y: 219 }) // back of green plane |     await sketchOnPlaneAndBackSideTest(page, homePage, '-YZ', { | ||||||
|  |       x: 700, | ||||||
|  |       y: 219, | ||||||
|  |     }) // back of green plan | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   test('-XZ', async ({ page }) => { |   test('-XZ', async ({ page, homePage }) => { | ||||||
|     await sketchOnPlaneAndBackSideTest(page, 'XZ', { x: 700, y: 427 }) // back of blue plane |     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 { | import { getUtils, executorInputPath } from './test-utils' | ||||||
|   getUtils, |  | ||||||
|   setup, |  | ||||||
|   setupElectron, |  | ||||||
|   tearDown, |  | ||||||
|   executorInputPath, |  | ||||||
| } from './test-utils' |  | ||||||
| import { join } from 'path' | import { join } from 'path' | ||||||
| import { bracket } from 'lib/exampleKcl' | import { bracket } from 'lib/exampleKcl' | ||||||
| import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates' | import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates' | ||||||
| import fsp from 'fs/promises' | 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.describe('Code pane and errors', () => { | ||||||
|   test('Typing KCL errors induces a badge on the code pane button', async ({ |   test('Typing KCL errors induces a badge on the code pane button', async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|  |  | ||||||
| @ -31,18 +18,18 @@ test.describe('Code pane and errors', () => { | |||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `// Extruded Triangle |         `// Extruded Triangle | ||||||
| sketch001 = startSketchOn('XZ') |   sketch001 = startSketchOn('XZ') | ||||||
|   |> startProfileAt([0, 0], %) |     |> startProfileAt([0, 0], %) | ||||||
|   |> line([10, 0], %) |     |> line([10, 0], %) | ||||||
|   |> line([-5, 10], %) |     |> line([-5, 10], %) | ||||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) |     |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||||
|   |> close(%) |     |> close(%) | ||||||
| extrude001 = extrude(5, sketch001)` |   extrude001 = extrude(5, sketch001)` | ||||||
|       ) |       ) | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await page.setViewportSize({ width: 1200, height: 500 }) |     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|     await u.waitForAuthSkipAppStart() |     await homePage.goToModelingScene() | ||||||
|  |  | ||||||
|     // wait for execution done |     // wait for execution done | ||||||
|     await u.openDebugPanel() |     await u.openDebugPanel() | ||||||
| @ -62,11 +49,11 @@ extrude001 = extrude(5, sketch001)` | |||||||
|     await expect(codePaneButtonHolder).toContainText('notification') |     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, |     page, | ||||||
|  |     homePage, | ||||||
|  |     editor, | ||||||
|   }) => { |   }) => { | ||||||
|     await page.goto('http://localhost:3000') |  | ||||||
|  |  | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|  |  | ||||||
|     // Load the app with the working starter code |     // Load the app with the working starter code | ||||||
| @ -74,8 +61,8 @@ extrude001 = extrude(5, sketch001)` | |||||||
|       localStorage.setItem('persistCode', code) |       localStorage.setItem('persistCode', code) | ||||||
|     }, bracket) |     }, bracket) | ||||||
|  |  | ||||||
|     await page.setViewportSize({ width: 1200, height: 900 }) |     await page.setBodyDimensions({ width: 1200, height: 900 }) | ||||||
|     await u.waitForAuthSkipAppStart() |     await homePage.goToModelingScene() | ||||||
|  |  | ||||||
|     // wait for execution done |     // wait for execution done | ||||||
|     await u.openDebugPanel() |     await u.openDebugPanel() | ||||||
| @ -91,8 +78,9 @@ extrude001 = extrude(5, sketch001)` | |||||||
|     await expect(codePaneButtonHolder).not.toContainText('notification') |     await expect(codePaneButtonHolder).not.toContainText('notification') | ||||||
|  |  | ||||||
|     // Delete a character to break the KCL |     // Delete a character to break the KCL | ||||||
|     await u.openKclCodePanel() |     await editor.openPane() | ||||||
|     await page.getByText('thickness, bracketLeg1Sketch)').click() |     await editor.scrollToText('thickness, bracketLeg1Sketch)') | ||||||
|  |     await page.getByText('extrude(thickness, bracketLeg1Sketch)').click() | ||||||
|     await page.keyboard.press('Backspace') |     await page.keyboard.press('Backspace') | ||||||
|  |  | ||||||
|     // Ensure that a badge appears on the button |     // 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() |     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||||
|  |  | ||||||
|     // Open the code pane |     // 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 |     // Ensure that a badge appears on the button | ||||||
|     await expect(codePaneButtonHolder).toContainText('notification') |     await expect(codePaneButtonHolder).toContainText('notification') | ||||||
| @ -129,59 +120,58 @@ extrude001 = extrude(5, sketch001)` | |||||||
|     await expect(page.locator('.cm-tooltip').first()).toBeVisible() |     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 ({ |   test.fixme( | ||||||
|     page, |     'When error is not in view you can click the badge to scroll to it', | ||||||
|   }) => { |     async ({ page, homePage, context }) => { | ||||||
|     const u = await getUtils(page) |       // 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.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|     await page.addInitScript((code) => { |       await homePage.goToModelingScene() | ||||||
|       localStorage.setItem('persistCode', code) |  | ||||||
|     }, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW) |  | ||||||
|  |  | ||||||
|     await page.setViewportSize({ width: 1200, height: 500 }) |       await page.waitForTimeout(1000) | ||||||
|     await u.waitForAuthSkipAppStart() |  | ||||||
|  |  | ||||||
|     await page.waitForTimeout(1000) |       // Ensure badge is present | ||||||
|  |       const codePaneButtonHolder = page.locator('#code-button-holder') | ||||||
|  |       await expect(codePaneButtonHolder).toContainText('notification') | ||||||
|  |  | ||||||
|     // Ensure badge is present |       // Ensure we have no errors in the gutter, since error out of view. | ||||||
|     const codePaneButtonHolder = page.locator('#code-button-holder') |       await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||||
|     await expect(codePaneButtonHolder).toContainText('notification') |  | ||||||
|  |  | ||||||
|     // Ensure we have no errors in the gutter, since error out of view. |       // Click the badge. | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() |       const badge = page.locator('#code-badge') | ||||||
|  |       await expect(badge).toBeVisible() | ||||||
|  |       await badge.click() | ||||||
|  |  | ||||||
|     // Click the badge. |       // Ensure we have an error diagnostic. | ||||||
|     const badge = page.locator('#code-badge') |       await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() | ||||||
|     await expect(badge).toBeVisible() |  | ||||||
|     await badge.click() |  | ||||||
|  |  | ||||||
|     // Ensure we have an error diagnostic. |       // Hover over the error to see the error message | ||||||
|     await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() |       await page.hover('.cm-lint-marker-error') | ||||||
|  |       await expect( | ||||||
|     // Hover over the error to see the error message |         page | ||||||
|     await page.hover('.cm-lint-marker-error') |           .getByText( | ||||||
|     await expect( |             'Modeling command failed: [ApiError { error_code: InternalEngine, message: "Solid3D revolve failed:  sketch profile must lie entirely on one side of the revolution axis" }]' | ||||||
|       page |           ) | ||||||
|         .getByText( |           .first() | ||||||
|           'sketch profile must lie entirely on one side of the revolution axis' |       ).toBeVisible() | ||||||
|         ) |     } | ||||||
|         .first() |   ) | ||||||
|     ).toBeVisible() |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   test('When error is not in view WITH LINTS you can click the badge to scroll to it', async ({ |   test('When error is not in view WITH LINTS you can click the badge to scroll to it', async ({ | ||||||
|  |     context, | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const u = await getUtils(page) |  | ||||||
|  |  | ||||||
|     // Load the app with the working starter code |     // Load the app with the working starter code | ||||||
|     await page.addInitScript((code) => { |     await context.addInitScript((code) => { | ||||||
|       localStorage.setItem('persistCode', code) |       localStorage.setItem('persistCode', code) | ||||||
|     }, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW) |     }, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW) | ||||||
|  |  | ||||||
|     await page.setViewportSize({ width: 1200, height: 500 }) |     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|     await u.waitForAuthSkipAppStart() |     await homePage.goToModelingScene() | ||||||
|  |  | ||||||
|     await page.waitForTimeout(1000) |     await page.waitForTimeout(1000) | ||||||
|  |  | ||||||
| @ -241,32 +231,29 @@ extrude001 = extrude(5, sketch001)` | |||||||
| test( | test( | ||||||
|   'Opening multiple panes persists when switching projects', |   'Opening multiple panes persists when switching projects', | ||||||
|   { tag: '@electron' }, |   { tag: '@electron' }, | ||||||
|   async ({ browserName }, testInfo) => { |   async ({ context, page }, testInfo) => { | ||||||
|     // Setup multiple projects. |     // Setup multiple projects. | ||||||
|     const { electronApp, page } = await setupElectron({ |     await context.folderSetupFn(async (dir) => { | ||||||
|       testInfo, |       const routerTemplateDir = join(dir, 'router-template-slate') | ||||||
|       folderSetupFn: async (dir) => { |       const bracketDir = join(dir, 'bracket') | ||||||
|         const routerTemplateDir = join(dir, 'router-template-slate') |       await Promise.all([ | ||||||
|         const bracketDir = join(dir, 'bracket') |         fsp.mkdir(routerTemplateDir, { recursive: true }), | ||||||
|         await Promise.all([ |         fsp.mkdir(bracketDir, { recursive: true }), | ||||||
|           fsp.mkdir(routerTemplateDir, { recursive: true }), |       ]) | ||||||
|           fsp.mkdir(bracketDir, { recursive: true }), |       await Promise.all([ | ||||||
|         ]) |         fsp.copyFile( | ||||||
|         await Promise.all([ |           executorInputPath('router-template-slate.kcl'), | ||||||
|           fsp.copyFile( |           join(routerTemplateDir, 'main.kcl') | ||||||
|             executorInputPath('router-template-slate.kcl'), |         ), | ||||||
|             join(routerTemplateDir, 'main.kcl') |         fsp.copyFile( | ||||||
|           ), |           executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||||
|           fsp.copyFile( |           join(bracketDir, 'main.kcl') | ||||||
|             executorInputPath('focusrite_scarlett_mounting_braket.kcl'), |         ), | ||||||
|             join(bracketDir, 'main.kcl') |       ]) | ||||||
|           ), |  | ||||||
|         ]) |  | ||||||
|       }, |  | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     const u = await getUtils(page) |     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 test.step('Opening the bracket project should load', async () => { | ||||||
|       await expect(page.getByText('bracket')).toBeVisible() |       await expect(page.getByText('bracket')).toBeVisible() | ||||||
| @ -293,7 +280,7 @@ test( | |||||||
|  |  | ||||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() |       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||||
|       await expect(page.getByText('router-template-slate')).toBeVisible() |       await expect(page.getByText('router-template-slate')).toBeVisible() | ||||||
|       await expect(page.getByText('New Project')).toBeVisible() |       await expect(page.getByText('Create project')).toBeVisible() | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await test.step('Opening the router-template project should load', async () => { |     await test.step('Opening the router-template project should load', async () => { | ||||||
| @ -309,30 +296,21 @@ test( | |||||||
|       await expect(page.locator('#variables-pane')).toBeVisible() |       await expect(page.locator('#variables-pane')).toBeVisible() | ||||||
|       await expect(page.locator('#logs-pane')).toBeVisible() |       await expect(page.locator('#logs-pane')).toBeVisible() | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await electronApp.close() |  | ||||||
|   } |   } | ||||||
| ) | ) | ||||||
|  |  | ||||||
| test( | test( | ||||||
|   'external change of file contents are reflected in editor', |   'external change of file contents are reflected in editor', | ||||||
|   { tag: '@electron' }, |   { tag: '@electron' }, | ||||||
|   async ({ browserName }, testInfo) => { |   async ({ context, page }, testInfo) => { | ||||||
|     const PROJECT_DIR_NAME = 'lee-was-here' |     const PROJECT_DIR_NAME = 'lee-was-here' | ||||||
|     const { |     const { dir: projectsDir } = await context.folderSetupFn(async (dir) => { | ||||||
|       electronApp, |       const aProjectDir = join(dir, PROJECT_DIR_NAME) | ||||||
|       page, |       await fsp.mkdir(aProjectDir, { recursive: true }) | ||||||
|       dir: projectsDir, |  | ||||||
|     } = await setupElectron({ |  | ||||||
|       testInfo, |  | ||||||
|       folderSetupFn: async (dir) => { |  | ||||||
|         const aProjectDir = join(dir, PROJECT_DIR_NAME) |  | ||||||
|         await fsp.mkdir(aProjectDir, { recursive: true }) |  | ||||||
|       }, |  | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     const u = await getUtils(page) |     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 test.step('Open the project', async () => { | ||||||
|       await expect(page.getByText(PROJECT_DIR_NAME)).toBeVisible() |       await expect(page.getByText(PROJECT_DIR_NAME)).toBeVisible() | ||||||
| @ -351,7 +329,5 @@ test( | |||||||
|       ) |       ) | ||||||
|       await u.editorTextMatches(content) |       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' | 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.describe('Command bar tests', () => { | ||||||
|   test('Extrude from command bar selects extrude line after', async ({ |   test('Extrude from command bar selects extrude line after', async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     await page.addInitScript(async () => { |     await page.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `sketch001 = startSketchOn('XY') |         `sketch001 = startSketchOn('XY') | ||||||
|     |> startProfileAt([-10, -10], %) |   |> startProfileAt([-10, -10], %) | ||||||
|     |> line([20, 0], %) |   |> line([20, 0], %) | ||||||
|     |> line([0, 20], %) |   |> line([0, 20], %) | ||||||
|     |> xLine(-20, %) |   |> xLine(-20, %) | ||||||
|     |> close(%) |   |> close(%) | ||||||
|       ` |     ` | ||||||
|       ) |       ) | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     const u = await getUtils(page) |     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.openDebugPanel() | ||||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') |     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||||
| @ -52,51 +45,12 @@ test.describe('Command bar tests', () => { | |||||||
|     ) |     ) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   test('Fillet from command bar', async ({ page }) => { |  | ||||||
|     await page.addInitScript(async () => { |  | ||||||
|       localStorage.setItem( |  | ||||||
|         'persistCode', |  | ||||||
|         `sketch001 = startSketchOn('XY') |  | ||||||
|   |> startProfileAt([-5, -5], %) |  | ||||||
|   |> line([0, 10], %) |  | ||||||
|   |> line([10, 0], %) |  | ||||||
|   |> line([0, -10], %) |  | ||||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) |  | ||||||
|   |> close(%) |  | ||||||
| extrude001 = extrude(-10, sketch001)` |  | ||||||
|       ) |  | ||||||
|     }) |  | ||||||
|  |  | ||||||
|     const u = await getUtils(page) |  | ||||||
|     await page.setViewportSize({ width: 1000, height: 500 }) |  | ||||||
|     await u.waitForAuthSkipAppStart() |  | ||||||
|     await u.openDebugPanel() |  | ||||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') |  | ||||||
|     await u.closeDebugPanel() |  | ||||||
|  |  | ||||||
|     const selectSegment = () => page.getByText(`line([0, -10], %)`).click() |  | ||||||
|  |  | ||||||
|     await selectSegment() |  | ||||||
|     await page.waitForTimeout(100) |  | ||||||
|     await page.getByRole('button', { name: 'Fillet' }).click() |  | ||||||
|     await page.waitForTimeout(100) |  | ||||||
|     await page.keyboard.press('Enter') // skip selection |  | ||||||
|     await page.waitForTimeout(100) |  | ||||||
|     await page.keyboard.press('Enter') // accept default radius |  | ||||||
|     await page.waitForTimeout(100) |  | ||||||
|     await page.keyboard.press('Enter') // submit |  | ||||||
|     await page.waitForTimeout(100) |  | ||||||
|     await expect(page.locator('.cm-activeLine')).toContainText( |  | ||||||
|       `fillet({ radius = ${KCL_DEFAULT_LENGTH}, tags = [seg01] }, %)` |  | ||||||
|     ) |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   test('Command bar can change a setting, and switch back and forth between arguments', async ({ |   test('Command bar can change a setting, and switch back and forth between arguments', async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const u = await getUtils(page) |     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|     await page.setViewportSize({ width: 1200, height: 500 }) |     await homePage.goToModelingScene() | ||||||
|     await u.waitForAuthSkipAppStart() |  | ||||||
|  |  | ||||||
|     const commandBarButton = page.getByRole('button', { name: 'Commands' }) |     const commandBarButton = page.getByRole('button', { name: 'Commands' }) | ||||||
|     const cmdSearchBar = page.getByPlaceholder('Search commands') |     const cmdSearchBar = page.getByPlaceholder('Search commands') | ||||||
| @ -153,7 +107,7 @@ extrude001 = extrude(-10, sketch001)` | |||||||
|     // Check that the visibility changed |     // Check that the visibility changed | ||||||
|     await expect(paneSelector).not.toBeVisible() |     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 |     // Test case for https://github.com/KittyCAD/modeling-app/issues/2882 | ||||||
|     await commandBarButton.click() |     await commandBarButton.click() | ||||||
| @ -174,10 +128,10 @@ extrude001 = extrude(-10, sketch001)` | |||||||
|  |  | ||||||
|   test('Command bar keybinding works from code editor and can change a setting', async ({ |   test('Command bar keybinding works from code editor and can change a setting', async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const u = await getUtils(page) |     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|     await page.setViewportSize({ width: 1200, height: 500 }) |     await homePage.goToModelingScene() | ||||||
|     await u.waitForAuthSkipAppStart() |  | ||||||
|  |  | ||||||
|     await expect( |     await expect( | ||||||
|       page.getByRole('button', { name: 'Start Sketch' }) |       page.getByRole('button', { name: 'Start Sketch' }) | ||||||
| @ -221,25 +175,25 @@ extrude001 = extrude(-10, sketch001)` | |||||||
|     await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) |     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 () => { |     await page.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `distance = sqrt(20) |         `distance = sqrt(20) | ||||||
|       sketch001 = startSketchOn('XZ') |     sketch001 = startSketchOn('XZ') | ||||||
|       |> startProfileAt([-6.95, 10.98], %) |     |> startProfileAt([-6.95, 10.98], %) | ||||||
|       |> line([25.1, 0.41], %) |     |> line([25.1, 0.41], %) | ||||||
|       |> line([0.73, -20.93], %) |     |> line([0.73, -20.93], %) | ||||||
|       |> line([-23.44, 0.52], %) |     |> line([-23.44, 0.52], %) | ||||||
|       |> close(%) |     |> close(%) | ||||||
|           ` |         ` | ||||||
|       ) |       ) | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     const u = await getUtils(page) |     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 |     // Make sure the stream is up | ||||||
|     await u.openDebugPanel() |     await u.openDebugPanel() | ||||||
| @ -293,26 +247,19 @@ extrude001 = extrude(-10, sketch001)` | |||||||
|     await continueButton.click() |     await continueButton.click() | ||||||
|     await submitButton.click() |     await submitButton.click() | ||||||
|  |  | ||||||
|     // Check that the code was updated |  | ||||||
|     await u.waitForCmdReceive('extrude') |     await u.waitForCmdReceive('extrude') | ||||||
|     // Unfortunately this indentation seems to matter for the test |  | ||||||
|     await expect(page.locator('.cm-content')).toHaveText( |     await expect(page.locator('.cm-content')).toContainText( | ||||||
|       `distance = sqrt(20) |       'extrude001 = extrude(distance001, sketch001)' | ||||||
| distance001 = ${KCL_DEFAULT_LENGTH} |  | ||||||
| sketch001 = startSketchOn('XZ') |  | ||||||
|     |> startProfileAt([-6.95, 10.98], %) |  | ||||||
|     |> line([25.1, 0.41], %) |  | ||||||
|     |> line([0.73, -20.93], %) |  | ||||||
|     |> line([-23.44, 0.52], %) |  | ||||||
|     |> close(%) |  | ||||||
| extrude001 = extrude(distance001, sketch001)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines |  | ||||||
|     ) |     ) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   test('Can switch between sketch tools via command bar', async ({ page }) => { |   test('Can switch between sketch tools via command bar', async ({ | ||||||
|     const u = await getUtils(page) |     page, | ||||||
|     await page.setViewportSize({ width: 1200, height: 500 }) |     homePage, | ||||||
|     await u.waitForAuthSkipAppStart() |   }) => { | ||||||
|  |     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|  |     await homePage.goToModelingScene() | ||||||
|  |  | ||||||
|     const sketchButton = page.getByRole('button', { name: 'Start Sketch' }) |     const sketchButton = page.getByRole('button', { name: 'Start Sketch' }) | ||||||
|     const cmdBarButton = page.getByRole('button', { name: 'Commands' }) |     const cmdBarButton = page.getByRole('button', { name: 'Commands' }) | ||||||
|  | |||||||
| @ -1,23 +1,16 @@ | |||||||
| import { test, expect } from '@playwright/test' | import { test, expect } from './zoo-test' | ||||||
| import { getUtils, setup, tearDown } from './test-utils' | 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', () => { | test.describe('Copilot ghost text', () => { | ||||||
|   // eslint-disable-next-line jest/valid-title |   // eslint-disable-next-line jest/valid-title | ||||||
|   test.skip(true, 'Needs to get covered again') |   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 u = await getUtils(page) | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |     // 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 u.codeLocator.click() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     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 ({ |   test.skip('copilot disabled in sketch mode no select plane', async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |     // 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 u.codeLocator.click() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     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 ({ |   test('copilot disabled in sketch mode after selecting plane', async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |     // 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 u.codeLocator.click() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     await expect(page.locator('.cm-content')).toHaveText(``) | ||||||
| @ -184,12 +179,12 @@ test.describe('Copilot ghost text', () => { | |||||||
|     await expect(page.locator('.cm-ghostText')).not.toBeVisible() |     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 u = await getUtils(page) | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |     // 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 u.codeLocator.click() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     await expect(page.locator('.cm-content')).toHaveText(``) | ||||||
| @ -212,12 +207,15 @@ test.describe('Copilot ghost text', () => { | |||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     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 u = await getUtils(page) | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |     // 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 u.codeLocator.click() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     await expect(page.locator('.cm-content')).toHaveText(``) | ||||||
| @ -240,12 +238,15 @@ test.describe('Copilot ghost text', () => { | |||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     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 u = await getUtils(page) | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |     // 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 u.codeLocator.click() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     await expect(page.locator('.cm-content')).toHaveText(``) | ||||||
| @ -268,12 +269,15 @@ test.describe('Copilot ghost text', () => { | |||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     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 u = await getUtils(page) | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |     // 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 u.codeLocator.click() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     await expect(page.locator('.cm-content')).toHaveText(``) | ||||||
| @ -296,12 +300,12 @@ test.describe('Copilot ghost text', () => { | |||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     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 u = await getUtils(page) | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |     // 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 u.codeLocator.click() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     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 u = await getUtils(page) | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |     // 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 u.codeLocator.click() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |     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 ({ |   test('Ctrl+z in code rejects the suggestion and undos the last code', async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |     // 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 page.waitForTimeout(800) | ||||||
|     await u.codeLocator.click() |     await u.codeLocator.click() | ||||||
| @ -420,98 +428,107 @@ test.describe('Copilot ghost text', () => { | |||||||
|     await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() |     await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() | ||||||
|  |  | ||||||
|     // TODO when we make codemirror a widget, we can test this. |     // 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 }) => { |     test('delete in code rejects the suggestion', async ({ | ||||||
|     const u = await getUtils(page) |       page, | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |       homePage, | ||||||
|     await page.setViewportSize({ width: 1200, height: 500 }) |     }) => { | ||||||
|  |       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 u.codeLocator.click() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |       await expect(page.locator('.cm-content')).toHaveText(``) | ||||||
|  |  | ||||||
|     await expect(page.locator('.cm-ghostText')).not.toBeVisible() |       await expect(page.locator('.cm-ghostText')).not.toBeVisible() | ||||||
|     await page.waitForTimeout(500) |       await page.waitForTimeout(500) | ||||||
|     await page.keyboard.press('Enter') |       await page.keyboard.press('Enter') | ||||||
|     await page.keyboard.press('Enter') |       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-ghostText').first()).toBeVisible() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText( |       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, %)` |         `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( |       await expect(page.locator('.cm-ghostText').first()).toHaveText( | ||||||
|       `fn cube = (pos, scale) => {` |         `fn cube = (pos, scale) => {` | ||||||
|     ) |       ) | ||||||
|  |  | ||||||
|     // Going elsewhere in the code should hide the ghost text. |       // Going elsewhere in the code should hide the ghost text. | ||||||
|     await page.keyboard.press('Delete') |       await page.keyboard.press('Delete') | ||||||
|     await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() |       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 }) => { |     test('backspace in code rejects the suggestion', async ({ | ||||||
|     const u = await getUtils(page) |       page, | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |       homePage, | ||||||
|     await page.setViewportSize({ width: 1200, height: 500 }) |     }) => { | ||||||
|  |       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 u.codeLocator.click() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |       await expect(page.locator('.cm-content')).toHaveText(``) | ||||||
|  |  | ||||||
|     await expect(page.locator('.cm-ghostText')).not.toBeVisible() |       await expect(page.locator('.cm-ghostText')).not.toBeVisible() | ||||||
|     await page.waitForTimeout(500) |       await page.waitForTimeout(500) | ||||||
|     await page.keyboard.press('Enter') |       await page.keyboard.press('Enter') | ||||||
|     await page.keyboard.press('Enter') |       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-ghostText').first()).toBeVisible() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText( |       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, %)` |         `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( |       await expect(page.locator('.cm-ghostText').first()).toHaveText( | ||||||
|       `fn cube = (pos, scale) => {` |         `fn cube = (pos, scale) => {` | ||||||
|     ) |       ) | ||||||
|  |  | ||||||
|     // Going elsewhere in the code should hide the ghost text. |       // Going elsewhere in the code should hide the ghost text. | ||||||
|     await page.keyboard.press('Backspace') |       await page.keyboard.press('Backspace') | ||||||
|     await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() |       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 }) => { |     test('focus outside code pane rejects the suggestion', async ({ | ||||||
|     const u = await getUtils(page) |       page, | ||||||
|     // const PUR = 400 / 37.5 //pixeltoUnitRatio |       homePage, | ||||||
|     await page.setViewportSize({ width: 1200, height: 500 }) |     }) => { | ||||||
|  |       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 u.codeLocator.click() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText(``) |       await expect(page.locator('.cm-content')).toHaveText(``) | ||||||
|  |  | ||||||
|     await expect(page.locator('.cm-ghostText')).not.toBeVisible() |       await expect(page.locator('.cm-ghostText')).not.toBeVisible() | ||||||
|     await page.waitForTimeout(500) |       await page.waitForTimeout(500) | ||||||
|     await page.keyboard.press('Enter') |       await page.keyboard.press('Enter') | ||||||
|     await expect(page.locator('.cm-ghostText').first()).toBeVisible() |       await expect(page.locator('.cm-ghostText').first()).toBeVisible() | ||||||
|     await expect(page.locator('.cm-content')).toHaveText( |       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, %)` |         `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( |       await expect(page.locator('.cm-ghostText').first()).toHaveText( | ||||||
|       `fn cube = (pos, scale) => {` |         `fn cube = (pos, scale) => {` | ||||||
|     ) |       ) | ||||||
|  |  | ||||||
|     // Going outside the editor should hide the ghost text. |       // Going outside the editor should hide the ghost text. | ||||||
|     await page.mouse.move(0, 0) |       await page.mouse.move(0, 0) | ||||||
|     await page |       await page | ||||||
|       .getByRole('button', { name: 'Start Sketch' }) |         .getByRole('button', { name: 'Start Sketch' }) | ||||||
|       .waitFor({ state: 'visible' }) |         .waitFor({ state: 'visible' }) | ||||||
|     await page.getByRole('button', { name: 'Start Sketch' }).click() |       await page.getByRole('button', { name: 'Start Sketch' }).click() | ||||||
|     await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() |       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' | 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) |  | ||||||
| }) |  | ||||||
|  |  | ||||||
| function countNewlines(input: string): number { | function countNewlines(input: string): number { | ||||||
|   let count = 0 |   let count = 0 | ||||||
| @ -24,13 +16,14 @@ test.describe('Debug pane', () => { | |||||||
|   test('Artifact IDs in the artifact graph are stable across code edits', async ({ |   test('Artifact IDs in the artifact graph are stable across code edits', async ({ | ||||||
|     page, |     page, | ||||||
|     context, |     context, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const code = `sketch001 = startSketchOn('XZ') |     const code = `sketch001 = startSketchOn('XZ') | ||||||
|   |> startProfileAt([0, 0], %) |     |> startProfileAt([0, 0], %) | ||||||
| |> line([1, 1], %) |   |> line([1, 1], %) | ||||||
| ` |   ` | ||||||
|     const u = await getUtils(page) |     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 tree = page.getByTestId('debug-feature-tree') | ||||||
|     const segment = tree.locator('li', { |     const segment = tree.locator('li', { | ||||||
| @ -39,20 +32,20 @@ test.describe('Debug pane', () => { | |||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await test.step('Test setup', async () => { |     await test.step('Test setup', async () => { | ||||||
|       await u.waitForAuthSkipAppStart() |       await homePage.goToModelingScene() | ||||||
|       await u.openKclCodePanel() |       await u.openKclCodePanel() | ||||||
|       await u.openDebugPanel() |       await u.openDebugPanel() | ||||||
|       // Set the code in the code editor. |       // Set the code in the code editor. | ||||||
|       await u.codeLocator.click() |       await u.codeLocator.click() | ||||||
|       await page.keyboard.type(code, { delay: 0 }) |       await page.keyboard.type(code, { delay: 0 }) | ||||||
|       // Scroll to the feature tree. |       // Scroll to the artifact graph. | ||||||
|       await tree.scrollIntoViewIfNeeded() |       await tree.scrollIntoViewIfNeeded() | ||||||
|       // Expand the feature tree. |       // Expand the artifact graph. | ||||||
|       await tree.getByText('Feature Tree').click() |       await tree.getByText('Artifact Graph').click() | ||||||
|       // Just expanded the details, making the element taller, so scroll again. |       // Just expanded the details, making the element taller, so scroll again. | ||||||
|       await tree.getByText('Plane').first().scrollIntoViewIfNeeded() |       await tree.getByText('Plane').first().scrollIntoViewIfNeeded() | ||||||
|     }) |     }) | ||||||
|     // Extract the artifact IDs from the debug feature tree. |     // Extract the artifact IDs from the debug artifact graph. | ||||||
|     const initialSegmentIds = await segment.innerText({ timeout: 5_000 }) |     const initialSegmentIds = await segment.innerText({ timeout: 5_000 }) | ||||||
|     // The artifact ID should include a UUID. |     // The artifact ID should include a UUID. | ||||||
|     expect(initialSegmentIds).toMatch( |     expect(initialSegmentIds).toMatch( | ||||||
|  | |||||||
| @ -1,39 +1,31 @@ | |||||||
| import { test, expect } from '@playwright/test' | import { test, expect } from './zoo-test' | ||||||
| import { join } from 'path' | import path from 'path' | ||||||
| import { | import { | ||||||
|   getUtils, |   getUtils, | ||||||
|   setupElectron, |  | ||||||
|   tearDown, |  | ||||||
|   executorInputPath, |   executorInputPath, | ||||||
|  |   getPlaywrightDownloadDir, | ||||||
| } from './test-utils' | } from './test-utils' | ||||||
| import fsp from 'fs/promises' | import fsp from 'fs/promises' | ||||||
|  |  | ||||||
| test.afterEach(async ({ page }, testInfo) => { |  | ||||||
|   await tearDown(page, testInfo) |  | ||||||
| }) |  | ||||||
|  |  | ||||||
| test( | test( | ||||||
|   'export works on the first try', |   'export works on the first try', | ||||||
|   { tag: '@electron' }, |   { tag: '@electron' }, | ||||||
|   async ({ browserName }, testInfo) => { |   async ({ page, context }, testInfo) => { | ||||||
|     const { electronApp, page } = await setupElectron({ |     await context.folderSetupFn(async (dir) => { | ||||||
|       testInfo, |       const bracketDir = path.join(dir, 'bracket') | ||||||
|       folderSetupFn: async (dir) => { |       await Promise.all([fsp.mkdir(bracketDir, { recursive: true })]) | ||||||
|         const bracketDir = join(dir, 'bracket') |       await Promise.all([ | ||||||
|         await Promise.all([fsp.mkdir(bracketDir, { recursive: true })]) |         fsp.copyFile( | ||||||
|         await Promise.all([ |           executorInputPath('router-template-slate.kcl'), | ||||||
|           fsp.copyFile( |           path.join(bracketDir, 'other.kcl') | ||||||
|             executorInputPath('router-template-slate.kcl'), |         ), | ||||||
|             join(bracketDir, 'other.kcl') |         fsp.copyFile( | ||||||
|           ), |           executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||||
|           fsp.copyFile( |           path.join(bracketDir, 'main.kcl') | ||||||
|             executorInputPath('focusrite_scarlett_mounting_braket.kcl'), |         ), | ||||||
|             join(bracketDir, 'main.kcl') |       ]) | ||||||
|           ), |  | ||||||
|         ]) |  | ||||||
|       }, |  | ||||||
|     }) |     }) | ||||||
|     await page.setViewportSize({ width: 1200, height: 500 }) |     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|  |  | ||||||
|     page.on('console', console.log) |     page.on('console', console.log) | ||||||
|  |  | ||||||
| @ -93,12 +85,16 @@ test( | |||||||
|       await expect(successToastMessage).toBeVisible() |       await expect(successToastMessage).toBeVisible() | ||||||
|       await expect(exportingToastMessage).not.toBeVisible() |       await expect(exportingToastMessage).not.toBeVisible() | ||||||
|  |  | ||||||
|  |       const firstFileFullPath = path.resolve( | ||||||
|  |         getPlaywrightDownloadDir(page), | ||||||
|  |         exportFileName | ||||||
|  |       ) | ||||||
|       await test.step('Check the export size', async () => { |       await test.step('Check the export size', async () => { | ||||||
|         await expect |         await expect | ||||||
|           .poll( |           .poll( | ||||||
|             async () => { |             async () => { | ||||||
|               try { |               try { | ||||||
|                 const outputGltf = await fsp.readFile(exportFileName) |                 const outputGltf = await fsp.readFile(firstFileFullPath) | ||||||
|                 return outputGltf.byteLength |                 return outputGltf.byteLength | ||||||
|               } catch (e) { |               } catch (e) { | ||||||
|                 return 0 |                 return 0 | ||||||
| @ -107,9 +103,6 @@ test( | |||||||
|             { timeout: 15_000 } |             { timeout: 15_000 } | ||||||
|           ) |           ) | ||||||
|           .toBeGreaterThan(300_000) |           .toBeGreaterThan(300_000) | ||||||
|  |  | ||||||
|         // clean up exported file |  | ||||||
|         await fsp.rm(exportFileName) |  | ||||||
|       }) |       }) | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
| @ -170,12 +163,16 @@ test( | |||||||
|           expect(exportingToastMessage).not.toBeVisible(), |           expect(exportingToastMessage).not.toBeVisible(), | ||||||
|         ])) |         ])) | ||||||
|  |  | ||||||
|  |       const secondFileFullPath = path.resolve( | ||||||
|  |         getPlaywrightDownloadDir(page), | ||||||
|  |         exportFileName | ||||||
|  |       ) | ||||||
|       await test.step('Check the export size', async () => { |       await test.step('Check the export size', async () => { | ||||||
|         await expect |         await expect | ||||||
|           .poll( |           .poll( | ||||||
|             async () => { |             async () => { | ||||||
|               try { |               try { | ||||||
|                 const outputGltf = await fsp.readFile(exportFileName) |                 const outputGltf = await fsp.readFile(secondFileFullPath) | ||||||
|                 return outputGltf.byteLength |                 return outputGltf.byteLength | ||||||
|               } catch (e) { |               } catch (e) { | ||||||
|                 return 0 |                 return 0 | ||||||
| @ -184,13 +181,7 @@ test( | |||||||
|             { timeout: 15_000 } |             { timeout: 15_000 } | ||||||
|           ) |           ) | ||||||
|           .toBeGreaterThan(100_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 fsp from 'fs/promises' | ||||||
| import { uuidv4 } from 'lib/utils' | import { uuidv4 } from 'lib/utils' | ||||||
| import { | import { | ||||||
| @ -6,37 +6,27 @@ import { | |||||||
|   darkModePlaneColorXZ, |   darkModePlaneColorXZ, | ||||||
|   executorInputPath, |   executorInputPath, | ||||||
|   getUtils, |   getUtils, | ||||||
|   setup, |  | ||||||
|   setupElectron, |  | ||||||
|   tearDown, |  | ||||||
| } from './test-utils' | } from './test-utils' | ||||||
|  |  | ||||||
| import { join } from 'path' | 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.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) |     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 |     // check no error to begin with | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() |     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||||
|  |  | ||||||
|     await u.codeLocator.click() |     await u.codeLocator.click() | ||||||
|     await page.keyboard.type(`sketch001 = startSketchOn('XY') |     await page.keyboard.type(`sketch001 = startSketchOn('XY') | ||||||
|   |> startProfileAt([-10, -10], %) |     |> startProfileAt([-10, -10], %) | ||||||
|   |> line([20, 0], %) |     |> line([20, 0], %) | ||||||
|   |> line([0, 20], %) |     |> line([0, 20], %) | ||||||
|   |> line([-20, 0], %) |     |> line([-20, 0], %) | ||||||
|   |> close(%)`) |     |> close(%)`) | ||||||
|  |  | ||||||
|     await page.keyboard.down('ControlOrMeta') |     await page.keyboard.down('ControlOrMeta') | ||||||
|     await page.keyboard.press('/') |     await page.keyboard.press('/') | ||||||
| @ -44,11 +34,11 @@ test.describe('Editor tests', () => { | |||||||
|  |  | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XY') |       .toHaveText(`sketch001 = startSketchOn('XY') | ||||||
|     |> startProfileAt([-10, -10], %) |   |> startProfileAt([-10, -10], %) | ||||||
|     |> line([20, 0], %) |   |> line([20, 0], %) | ||||||
|     |> line([0, 20], %) |   |> line([0, 20], %) | ||||||
|     |> line([-20, 0], %) |   |> line([-20, 0], %) | ||||||
|     // |> close(%)`) |   // |> close(%)`) | ||||||
|  |  | ||||||
|     // uncomment the code |     // uncomment the code | ||||||
|     await page.keyboard.down('ControlOrMeta') |     await page.keyboard.down('ControlOrMeta') | ||||||
| @ -57,48 +47,22 @@ test.describe('Editor tests', () => { | |||||||
|  |  | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XY') |       .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', async ({ |  | ||||||
|     page, |  | ||||||
|   }) => { |  | ||||||
|     const u = await getUtils(page) |  | ||||||
|     await page.setViewportSize({ width: 1000, height: 500 }) |  | ||||||
|  |  | ||||||
|     await u.waitForAuthSkipAppStart() |  | ||||||
|  |  | ||||||
|     // check no error to begin with |  | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() |  | ||||||
|  |  | ||||||
|     await u.codeLocator.click() |  | ||||||
|     await page.keyboard.type(`sketch001 = startSketchOn('XY') |  | ||||||
|   |> startProfileAt([-10, -10], %) |   |> startProfileAt([-10, -10], %) | ||||||
|   |> line([20, 0], %) |   |> line([20, 0], %) | ||||||
|   |> line([0, 20], %) |   |> line([0, 20], %) | ||||||
|   |> line([-20, 0], %) |   |> line([-20, 0], %) | ||||||
|   |> close(%)`) |   |> 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('ensure we use the cache, and do not re-execute', async ({ page }) => { |   test('ensure we use the cache, and do not re-execute', async ({ | ||||||
|  |     homePage, | ||||||
|  |     page, | ||||||
|  |   }) => { | ||||||
|     const u = await getUtils(page) |     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() | ||||||
|  |     await u.waitForPageLoad() | ||||||
|  |  | ||||||
|     await u.codeLocator.click() |     await u.codeLocator.click() | ||||||
|     await page.keyboard.type(`sketch001 = startSketchOn('XY') |     await page.keyboard.type(`sketch001 = startSketchOn('XY') | ||||||
| @ -112,7 +76,7 @@ test.describe('Editor tests', () => { | |||||||
|     await u.openDebugPanel() |     await u.openDebugPanel() | ||||||
|     await expect( |     await expect( | ||||||
|       page.locator('[data-receive-command-type="scene_clear_all"]') |       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||||
|     ).toHaveCount(2) |     ).toHaveCount(1) | ||||||
|     await expect( |     await expect( | ||||||
|       page.locator('[data-message-type="execution-done"]') |       page.locator('[data-message-type="execution-done"]') | ||||||
|     ).toHaveCount(2) |     ).toHaveCount(2) | ||||||
| @ -136,27 +100,112 @@ test.describe('Editor tests', () => { | |||||||
|     ).toHaveCount(3) |     ).toHaveCount(3) | ||||||
|     await expect( |     await expect( | ||||||
|       page.locator('[data-receive-command-type="scene_clear_all"]') |       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||||
|  |     ).toHaveCount(1) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   test('ensure we use the cache, and do not clear on append', async ({ | ||||||
|  |     homePage, | ||||||
|  |     page, | ||||||
|  |   }) => { | ||||||
|  |     const u = await getUtils(page) | ||||||
|  |     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||||
|  |  | ||||||
|  |     await homePage.goToModelingScene() | ||||||
|  |     await u.waitForPageLoad() | ||||||
|  |  | ||||||
|  |     await u.codeLocator.click() | ||||||
|  |     await page.keyboard.type(`sketch001 = startSketchOn('XY') | ||||||
|  |   |> startProfileAt([-10, -10], %) | ||||||
|  |   |> line([20, 0], %) | ||||||
|  |   |> line([0, 20], %) | ||||||
|  |   |> line([-20, 0], %) | ||||||
|  |   |> close(%)`) | ||||||
|  |  | ||||||
|  |     // Ensure we execute the first time. | ||||||
|  |     await u.openDebugPanel() | ||||||
|  |     await expect( | ||||||
|  |       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||||
|  |     ).toHaveCount(1) | ||||||
|  |     await expect( | ||||||
|  |       page.locator('[data-message-type="execution-done"]') | ||||||
|     ).toHaveCount(2) |     ).toHaveCount(2) | ||||||
|  |  | ||||||
|  |     // Add whitespace to the end of the code. | ||||||
|  |     await u.codeLocator.click() | ||||||
|  |     await page.keyboard.press('ArrowDown') | ||||||
|  |     await page.keyboard.press('ArrowDown') | ||||||
|  |     await page.keyboard.press('ArrowDown') | ||||||
|  |     await page.keyboard.press('ArrowDown') | ||||||
|  |     await page.keyboard.press('ArrowDown') | ||||||
|  |     await page.keyboard.press('ArrowDown') | ||||||
|  |     await page.keyboard.press('ArrowDown') | ||||||
|  |     await page.keyboard.press('ArrowDown') | ||||||
|  |     await page.keyboard.press('ArrowDown') | ||||||
|  |     await page.keyboard.press('End') | ||||||
|  |     await page.keyboard.press('Enter') | ||||||
|  |     await page.keyboard.press('Enter') | ||||||
|  |     await page.keyboard.type('const x = 1') | ||||||
|  |     await page.keyboard.press('Enter') | ||||||
|  |  | ||||||
|  |     await u.openDebugPanel() | ||||||
|  |     await expect( | ||||||
|  |       page.locator('[data-message-type="execution-done"]') | ||||||
|  |     ).toHaveCount(3) | ||||||
|  |     await expect( | ||||||
|  |       page.locator('[data-receive-command-type="scene_clear_all"]') | ||||||
|  |     ).toHaveCount(1) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   test('if you click the format button it formats your code', async ({ | ||||||
|  |     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 ({ |   test('if you click the format button it formats your code and executes so lints are still there', async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const u = await getUtils(page) |     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 |     // check no error to begin with | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() |     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||||
|  |  | ||||||
|     await u.codeLocator.click() |     await u.codeLocator.click() | ||||||
|     await page.keyboard.type(`sketch_001 = startSketchOn('XY') |     await page.keyboard.type(`sketch_001 = startSketchOn('XY') | ||||||
|   |> startProfileAt([-10, -10], %) |     |> startProfileAt([-10, -10], %) | ||||||
|   |> line([20, 0], %) |     |> line([20, 0], %) | ||||||
|   |> line([0, 20], %) |     |> line([0, 20], %) | ||||||
|   |> line([-20, 0], %) |     |> line([-20, 0], %) | ||||||
|   |> close(%)`) |     |> close(%)`) | ||||||
|  |  | ||||||
|     await u.openDebugPanel() |     await u.openDebugPanel() | ||||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') |     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||||
| @ -180,11 +229,11 @@ test.describe('Editor tests', () => { | |||||||
|  |  | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch_001 = startSketchOn('XY') |       .toHaveText(`sketch_001 = startSketchOn('XY') | ||||||
|     |> startProfileAt([-10, -10], %) |   |> startProfileAt([-10, -10], %) | ||||||
|     |> line([20, 0], %) |   |> line([20, 0], %) | ||||||
|     |> line([0, 20], %) |   |> line([0, 20], %) | ||||||
|     |> line([-20, 0], %) |   |> line([-20, 0], %) | ||||||
|     |> close(%)`) |   |> close(%)`) | ||||||
|  |  | ||||||
|     // error in guter |     // error in guter | ||||||
|     await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() |     await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() | ||||||
| @ -196,29 +245,27 @@ test.describe('Editor tests', () => { | |||||||
|     ).toBeVisible() |     ).toBeVisible() | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   test('fold gutters work', async ({ page }) => { |   test('fold gutters work', async ({ page, homePage }) => { | ||||||
|     const u = await getUtils(page) |  | ||||||
|  |  | ||||||
|     const fullCode = `sketch001 = startSketchOn('XY') |     const fullCode = `sketch001 = startSketchOn('XY') | ||||||
|      |> startProfileAt([-10, -10], %) |    |> startProfileAt([-10, -10], %) | ||||||
|      |> line([20, 0], %) |    |> line([20, 0], %) | ||||||
|      |> line([0, 20], %) |    |> line([0, 20], %) | ||||||
|      |> line([-20, 0], %) |    |> line([-20, 0], %) | ||||||
|      |> close(%)` |    |> close(%)` | ||||||
|     await page.addInitScript(async () => { |     await page.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `sketch001 = startSketchOn('XY') |         `sketch001 = startSketchOn('XY') | ||||||
|      |> startProfileAt([-10, -10], %) |    |> startProfileAt([-10, -10], %) | ||||||
|      |> line([20, 0], %) |    |> line([20, 0], %) | ||||||
|      |> line([0, 20], %) |    |> line([0, 20], %) | ||||||
|      |> line([-20, 0], %) |    |> line([-20, 0], %) | ||||||
|      |> close(%)` |    |> 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 |     // TODO: Jess needs to fix this but you have to mod the code to get them to show | ||||||
|     // up, its an annoying codemirror thing. |     // up, its an annoying codemirror thing. | ||||||
| @ -269,22 +316,25 @@ test.describe('Editor tests', () => { | |||||||
|     await expect(foldGutterFoldLine).not.toBeVisible() |     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) |     const u = await getUtils(page) | ||||||
|     await page.addInitScript(async () => { |     await page.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `sketch001 = startSketchOn('XY') |         `sketch001 = startSketchOn('XY') | ||||||
|   |> startProfileAt([-10, -10], %) |     |> startProfileAt([-10, -10], %) | ||||||
|   |> line([20, 0], %) |     |> line([20, 0], %) | ||||||
|   |> line([0, 20], %) |     |> line([0, 20], %) | ||||||
|   |> line([-20, 0], %) |     |> line([-20, 0], %) | ||||||
|   |> close(%)` |     |> 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 |     // check no error to begin with | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() |     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||||
| @ -313,23 +363,24 @@ test.describe('Editor tests', () => { | |||||||
|  |  | ||||||
|   test('if you use the format keyboard binding it formats your code', async ({ |   test('if you use the format keyboard binding it formats your code', async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|     await page.addInitScript(async () => { |     await page.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `sketch001 = startSketchOn('XY') |         `sketch001 = startSketchOn('XY') | ||||||
|   |> startProfileAt([-10, -10], %) |     |> startProfileAt([-10, -10], %) | ||||||
|   |> line([20, 0], %) |     |> line([20, 0], %) | ||||||
|   |> line([0, 20], %) |     |> line([0, 20], %) | ||||||
|   |> line([-20, 0], %) |     |> line([-20, 0], %) | ||||||
|   |> close(%)` |     |> close(%)` | ||||||
|       ) |       ) | ||||||
|       localStorage.setItem('disableAxis', 'true') |       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 |     // check no error to begin with | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() |     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||||
| @ -346,32 +397,33 @@ test.describe('Editor tests', () => { | |||||||
|  |  | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XY') |       .toHaveText(`sketch001 = startSketchOn('XY') | ||||||
|     |> startProfileAt([-10, -10], %) |   |> startProfileAt([-10, -10], %) | ||||||
|     |> line([20, 0], %) |   |> line([20, 0], %) | ||||||
|     |> line([0, 20], %) |   |> line([0, 20], %) | ||||||
|     |> line([-20, 0], %) |   |> line([-20, 0], %) | ||||||
|     |> close(%)`) |   |> close(%)`) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   test('if you use the format keyboard binding it formats your code and executes so lints are shown', async ({ |   test('if you use the format keyboard binding it formats your code and executes so lints are shown', async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|     await page.addInitScript(async () => { |     await page.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `sketch_001 = startSketchOn('XY') |         `sketch_001 = startSketchOn('XY') | ||||||
|   |> startProfileAt([-10, -10], %) |     |> startProfileAt([-10, -10], %) | ||||||
|   |> line([20, 0], %) |     |> line([20, 0], %) | ||||||
|   |> line([0, 20], %) |     |> line([0, 20], %) | ||||||
|   |> line([-20, 0], %) |     |> line([-20, 0], %) | ||||||
|   |> close(%)` |     |> close(%)` | ||||||
|       ) |       ) | ||||||
|       localStorage.setItem('disableAxis', 'true') |       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.openDebugPanel() | ||||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') |     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||||
| @ -398,11 +450,11 @@ test.describe('Editor tests', () => { | |||||||
|  |  | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch_001 = startSketchOn('XY') |       .toHaveText(`sketch_001 = startSketchOn('XY') | ||||||
|     |> startProfileAt([-10, -10], %) |   |> startProfileAt([-10, -10], %) | ||||||
|     |> line([20, 0], %) |   |> line([20, 0], %) | ||||||
|     |> line([0, 20], %) |   |> line([0, 20], %) | ||||||
|     |> line([-20, 0], %) |   |> line([-20, 0], %) | ||||||
|     |> close(%)`) |   |> close(%)`) | ||||||
|  |  | ||||||
|     // error in guter |     // error in guter | ||||||
|     await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() |     await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() | ||||||
| @ -414,11 +466,14 @@ test.describe('Editor tests', () => { | |||||||
|     ).toBeVisible() |     ).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) |     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 |     // check no error to begin with | ||||||
|     await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible() |     await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible() | ||||||
| @ -454,23 +509,26 @@ test.describe('Editor tests', () => { | |||||||
|     await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible() |     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) |     const u = await getUtils(page) | ||||||
|     await page.addInitScript(async () => { |     await page.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `sketch001 = startSketchOn('XZ') |         `sketch001 = startSketchOn('XZ') | ||||||
|   |> startProfileAt([3.29, 7.86], %) |     |> startProfileAt([3.29, 7.86], %) | ||||||
|   |> line([2.48, 2.44], %) |     |> line([2.48, 2.44], %) | ||||||
|   |> line([2.66, 1.17], %) |     |> line([2.66, 1.17], %) | ||||||
|   |> close(%) |     |> 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 |     // check no error to begin with | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() |     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||||
| @ -492,22 +550,27 @@ test.describe('Editor tests', () => { | |||||||
|     ).not.toBeVisible() |     ).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) |     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 |     // check no error to begin with | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() |     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||||
|  |  | ||||||
|     /* add the following code to the editor ($ error is not a valid line) |     /* add the following code to the editor (~ error is not a valid line) | ||||||
|       $ error |       * the old check here used $ but this is for tags so it changed meaning. | ||||||
|       topAng = 30 |       * hopefully ~ doesn't change meaning | ||||||
|       bottomAng = 25 |     ~ error | ||||||
|      */ |     const topAng = 30 | ||||||
|  |     const bottomAng = 25 | ||||||
|  |    */ | ||||||
|     await u.codeLocator.click() |     await u.codeLocator.click() | ||||||
|     await page.keyboard.type('$ error') |     await page.keyboard.type('~ error') | ||||||
|  |  | ||||||
|     // press arrows to clear autocomplete |     // press arrows to clear autocomplete | ||||||
|     await page.keyboard.press('ArrowLeft') |     await page.keyboard.press('ArrowLeft') | ||||||
| @ -519,17 +582,17 @@ test.describe('Editor tests', () => { | |||||||
|     await page.keyboard.type('bottomAng = 25') |     await page.keyboard.type('bottomAng = 25') | ||||||
|     await page.keyboard.press('Enter') |     await page.keyboard.press('Enter') | ||||||
|  |  | ||||||
|     // error in gutter |     // error in guter | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).toBeVisible() |     await expect(page.locator('.cm-lint-marker-error')).toBeVisible() | ||||||
|  |  | ||||||
|     // error text on hover |     // error text on hover | ||||||
|     await page.hover('.cm-lint-marker-error') |     await page.hover('.cm-lint-marker-error') | ||||||
|     await expect( |     await expect( | ||||||
|       page.getByText('Tag names must not be empty').first() |       page.getByText("found unknown token '~'").first() | ||||||
|     ).toBeVisible() |     ).toBeVisible() | ||||||
|  |  | ||||||
|     // select the line that's causing the error and delete it |     // 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.press('End') | ||||||
|     await page.keyboard.down('Shift') |     await page.keyboard.down('Shift') | ||||||
|     await page.keyboard.press('Home') |     await page.keyboard.press('Home') | ||||||
| @ -565,106 +628,108 @@ test.describe('Editor tests', () => { | |||||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() |     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   // TODO currently multiple source ranges are not supported |   test.fixme( | ||||||
|   test.skip('error with 2 source ranges gets 2 diagnostics', async ({ |     'error with 2 source ranges gets 2 diagnostics', | ||||||
|     page, |     async ({ page, homePage }) => { | ||||||
|   }) => { |       const u = await getUtils(page) | ||||||
|     const u = await getUtils(page) |       await page.addInitScript(async () => { | ||||||
|     await page.addInitScript(async () => { |         localStorage.setItem( | ||||||
|       localStorage.setItem( |           'persistCode', | ||||||
|         'persistCode', |           `length = .750 | ||||||
|         `length = .750 |     width = 0.500 | ||||||
|   width = 0.500 |     height = 0.500 | ||||||
|   height = 0.500 |     dia = 4 | ||||||
|   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) => { |       await homePage.goToModelingScene() | ||||||
|     squareHoleSketch = startSketchOn('XY') |       await u.waitForPageLoad() | ||||||
|     |> startProfileAt([-width / 2, -length / 2], %) |       await page.waitForTimeout(1000) | ||||||
|     |> lineTo([width / 2, -length / 2], %) |  | ||||||
|     |> lineTo([width / 2, length / 2], %) |  | ||||||
|     |> lineTo([-width / 2, length / 2], %) |  | ||||||
|     |> close(%) |  | ||||||
|     return squareHoleSketch |  | ||||||
|   } |  | ||||||
|   ` |  | ||||||
|       ) |  | ||||||
|     }) |  | ||||||
|     await page.setViewportSize({ width: 1000, height: 500 }) |  | ||||||
|  |  | ||||||
|     await u.waitForAuthSkipAppStart() |       await u.openDebugPanel() | ||||||
|  |       await u.expectCmdLog('[data-message-type="execution-done"]') | ||||||
|  |       await u.closeDebugPanel() | ||||||
|  |  | ||||||
|     await u.openDebugPanel() |       // check no error to begin with | ||||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') |       await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||||
|     await u.closeDebugPanel() |  | ||||||
|  |  | ||||||
|     // check no error to begin with |       // Click on the bottom of the code editor to add a new line | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() |       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 |       // error in gutter | ||||||
|     await u.codeLocator.click() |       await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() | ||||||
|     await page.keyboard.press('ArrowDown') |       await page.hover('.cm-lint-marker-error:first-child') | ||||||
|     await page.keyboard.press('ArrowDown') |       await expect( | ||||||
|     await page.keyboard.press('ArrowDown') |         page.getByText('Expected 2 arguments, got 3').first() | ||||||
|     await page.keyboard.press('ArrowDown') |       ).toBeVisible() | ||||||
|     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 |       // Make sure there are two diagnostics | ||||||
|     await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() |       await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2) | ||||||
|     await page.hover('.cm-lint-marker-error:first-child') |     } | ||||||
|     await expect( |   ) | ||||||
|       page.getByText('Expected 2 arguments, got 3').first() |  | ||||||
|     ).toBeVisible() |  | ||||||
|  |  | ||||||
|     // Make sure there are two diagnostics |  | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2) |  | ||||||
|   }) |  | ||||||
|   test('if your kcl gets an error from the engine it is inlined', async ({ |   test('if your kcl gets an error from the engine it is inlined', async ({ | ||||||
|  |     context, | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const u = await getUtils(page) |     await context.addInitScript(async () => { | ||||||
|     await page.addInitScript(async () => { |  | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `box = startSketchOn('XY') |         `box = startSketchOn('XY') | ||||||
|   |> startProfileAt([0, 0], %) |     |> startProfileAt([0, 0], %) | ||||||
|   |> line([0, 10], %) |     |> line([0, 10], %) | ||||||
|   |> line([10, 0], %) |     |> line([10, 0], %) | ||||||
|   |> line([0, -10], %, $revolveAxis) |     |> line([0, -10], %, $revolveAxis) | ||||||
|   |> close(%) |     |> close(%) | ||||||
|   |> extrude(10, %) |     |> extrude(10, %) | ||||||
|  |    | ||||||
|   sketch001 = startSketchOn(box, revolveAxis) |     sketch001 = startSketchOn(box, revolveAxis) | ||||||
|   |> startProfileAt([5, 10], %) |     |> startProfileAt([5, 10], %) | ||||||
|   |> line([0, -10], %) |     |> line([0, -10], %) | ||||||
|   |> line([2, 0], %) |     |> line([2, 0], %) | ||||||
|   |> line([0, -10], %) |     |> line([0, -10], %) | ||||||
|   |> close(%) |     |> close(%) | ||||||
|   |> revolve({ |     |> revolve({ | ||||||
|   axis = revolveAxis, |     axis: revolveAxis, | ||||||
|   angle = 90 |     angle: 90 | ||||||
|   }, %) |     }, %) | ||||||
|       ` |     ` | ||||||
|       ) |       ) | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await page.setViewportSize({ width: 1000, height: 500 }) |     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||||
|  |  | ||||||
|     await page.goto('/') |     await homePage.goToModelingScene() | ||||||
|     await u.waitForPageLoad() |  | ||||||
|  |  | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).toBeVisible() |     await expect(page.locator('.cm-lint-marker-error')).toBeVisible() | ||||||
|  |  | ||||||
| @ -675,12 +740,15 @@ test.describe('Editor tests', () => { | |||||||
|     await expect(page.getByText(searchText)).toBeVisible() |     await expect(page.getByText(searchText)).toBeVisible() | ||||||
|   }) |   }) | ||||||
|   test.describe('Autocomplete works', () => { |   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 u = await getUtils(page) | ||||||
|       // const PUR = 400 / 37.5 //pixeltoUnitRatio |       // 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 |       // tests clicking on an option, selection the first option | ||||||
|       // and arrowing down to an option |       // and arrowing down to an option | ||||||
| @ -742,19 +810,19 @@ test.describe('Editor tests', () => { | |||||||
|  |  | ||||||
|       await expect(page.locator('.cm-content')) |       await expect(page.locator('.cm-content')) | ||||||
|         .toHaveText(`sketch001 = startSketchOn('XZ') |         .toHaveText(`sketch001 = startSketchOn('XZ') | ||||||
|     |> startProfileAt([3.14, 12], %) |         |> startProfileAt([3.14, 12], %) | ||||||
|     |> xLine(5, %) // lin`) |         |> xLine(5, %) // lin`) | ||||||
|  |  | ||||||
|       // expect there to be no KCL errors |       // expect there to be no KCL errors | ||||||
|       await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0) |       await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0) | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     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 u = await getUtils(page) | ||||||
|       // const PUR = 400 / 37.5 //pixeltoUnitRatio |       // 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 |       // this test might be brittle as we add and remove functions | ||||||
|       // but should also be easy to update. |       // but should also be easy to update. | ||||||
| @ -816,26 +884,30 @@ test.describe('Editor tests', () => { | |||||||
|  |  | ||||||
|       await expect(page.locator('.cm-content')) |       await expect(page.locator('.cm-content')) | ||||||
|         .toHaveText(`sketch001 = startSketchOn('XZ') |         .toHaveText(`sketch001 = startSketchOn('XZ') | ||||||
|     |> startProfileAt([3.14, 12], %) |         |> startProfileAt([3.14, 12], %) | ||||||
|     |> xLine(5, %) // lin`) |         |> 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) |     const u = await getUtils(page) | ||||||
|     await page.addInitScript(async () => { |     await context.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `sketch001 = startSketchOn('XZ') |         `sketch001 = startSketchOn('XZ') | ||||||
|     |> startProfileAt([4.61, -14.01], %) |   |> startProfileAt([4.61, -14.01], %) | ||||||
|     |> line([12.73, -0.09], %) |   |> line([12.73, -0.09], %) | ||||||
|     |> tangentialArcTo([24.95, -5.38], %) |   |> tangentialArcTo([24.95, -5.38], %) | ||||||
|     |> close(%)` |   |> close(%)` | ||||||
|       ) |       ) | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await page.setViewportSize({ width: 1200, height: 500 }) |     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|  |  | ||||||
|     await u.waitForAuthSkipAppStart() |     await homePage.goToModelingScene() | ||||||
|     await expect( |     await expect( | ||||||
|       page.getByRole('button', { name: 'Start Sketch' }) |       page.getByRole('button', { name: 'Start Sketch' }) | ||||||
|     ).not.toBeDisabled() |     ).not.toBeDisabled() | ||||||
| @ -888,29 +960,32 @@ test.describe('Editor tests', () => { | |||||||
|     await page.waitForTimeout(100) |     await page.waitForTimeout(100) | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') |       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||||
|     |> startProfileAt([4.61, -14.01], %) |   |> startProfileAt([4.61, -14.01], %) | ||||||
|     |> line([12.73, -0.09], %) |   |> line([12.73, -0.09], %) | ||||||
|     |> tangentialArcTo([24.95, -5.38], %) |   |> tangentialArcTo([24.95, -5.38], %) | ||||||
|     |> close(%)`) |   |> 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) |     const u = await getUtils(page) | ||||||
|     await page.addInitScript(async () => { |     await page.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `sketch001 = startSketchOn('XZ') |         `sketch001 = startSketchOn('XZ') | ||||||
|     |> startProfileAt([4.61, -10.01], %) |   |> startProfileAt([4.61, -10.01], %) | ||||||
|     |> line([12.73, -0.09], %) |   |> line([12.73, -0.09], %) | ||||||
|     |> tangentialArcTo([24.95, -0.38], %) |   |> tangentialArcTo([24.95, -0.38], %) | ||||||
|     |> close(%) |   |> close(%) | ||||||
|     |> extrude(5, %)` |   |> extrude(5, %)` | ||||||
|       ) |       ) | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await page.setViewportSize({ width: 1200, height: 500 }) |     await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|  |  | ||||||
|     await u.waitForAuthSkipAppStart() |     await homePage.goToModelingScene() | ||||||
|     await expect( |     await expect( | ||||||
|       page.getByRole('button', { name: 'Start Sketch' }) |       page.getByRole('button', { name: 'Start Sketch' }) | ||||||
|     ).not.toBeDisabled() |     ).not.toBeDisabled() | ||||||
| @ -937,7 +1012,7 @@ test.describe('Editor tests', () => { | |||||||
|     }) |     }) | ||||||
|     await page.waitForTimeout(100) |     await page.waitForTimeout(100) | ||||||
|  |  | ||||||
|     const startPX = [665, 397] |     const startPX = [1200 / 2, 500 / 2] | ||||||
|  |  | ||||||
|     const dragPX = 40 |     const dragPX = 40 | ||||||
|  |  | ||||||
| @ -951,9 +1026,9 @@ test.describe('Editor tests', () => { | |||||||
|  |  | ||||||
|     await expect(page.getByTestId('segment-overlay')).toHaveCount(2) |     await expect(page.getByTestId('segment-overlay')).toHaveCount(2) | ||||||
|  |  | ||||||
|     // drag startProfieAt handle |     // drag startProfileAt handle | ||||||
|     await page.dragAndDrop('#stream', '#stream', { |     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 }, |       targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX }, | ||||||
|     }) |     }) | ||||||
|     await page.waitForTimeout(100) |     await page.waitForTimeout(100) | ||||||
| @ -991,12 +1066,12 @@ test.describe('Editor tests', () => { | |||||||
|     // expect the code to have changed |     // expect the code to have changed | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') |       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||||
|   |> startProfileAt([7.12, -12.68], %) |     |> startProfileAt([2.71, -2.71], %) | ||||||
|   |> line([15.39, -2.78], %) |     |> line([15.4, -2.78], %) | ||||||
|   |> tangentialArcTo([27.6, -3.05], %) |     |> tangentialArcTo([27.6, -3.05], %) | ||||||
|   |> close(%) |     |> close(%) | ||||||
|   |> extrude(5, %) |     |> extrude(5, %) | ||||||
| `) |   `) | ||||||
|  |  | ||||||
|     // Hit undo |     // Hit undo | ||||||
|     await page.keyboard.down('Control') |     await page.keyboard.down('Control') | ||||||
| @ -1005,11 +1080,11 @@ test.describe('Editor tests', () => { | |||||||
|  |  | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') |       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||||
|   |> startProfileAt([7.12, -12.68], %) |     |> startProfileAt([2.71, -2.71], %) | ||||||
|   |> line([15.39, -2.78], %) |     |> line([15.4, -2.78], %) | ||||||
|   |> tangentialArcTo([24.95, -0.38], %) |     |> tangentialArcTo([24.95, -0.38], %) | ||||||
|   |> close(%) |     |> close(%) | ||||||
|   |> extrude(5, %)`) |     |> extrude(5, %)`) | ||||||
|  |  | ||||||
|     // Hit undo again. |     // Hit undo again. | ||||||
|     await page.keyboard.down('Control') |     await page.keyboard.down('Control') | ||||||
| @ -1018,12 +1093,12 @@ test.describe('Editor tests', () => { | |||||||
|  |  | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') |       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||||
|   |> startProfileAt([7.12, -12.68], %) |     |> startProfileAt([2.71, -2.71], %) | ||||||
|   |> line([12.73, -0.09], %) |     |> line([12.73, -0.09], %) | ||||||
|   |> tangentialArcTo([24.95, -0.38], %) |     |> tangentialArcTo([24.95, -0.38], %) | ||||||
|   |> close(%) |     |> close(%) | ||||||
|   |> extrude(5, %) |     |> extrude(5, %) | ||||||
| `) |   `) | ||||||
|  |  | ||||||
|     // Hit undo again. |     // Hit undo again. | ||||||
|     await page.keyboard.down('Control') |     await page.keyboard.down('Control') | ||||||
| @ -1033,31 +1108,29 @@ test.describe('Editor tests', () => { | |||||||
|     await page.waitForTimeout(100) |     await page.waitForTimeout(100) | ||||||
|     await expect(page.locator('.cm-content')) |     await expect(page.locator('.cm-content')) | ||||||
|       .toHaveText(`sketch001 = startSketchOn('XZ') |       .toHaveText(`sketch001 = startSketchOn('XZ') | ||||||
|     |> startProfileAt([4.61, -10.01], %) |   |> startProfileAt([4.61, -10.01], %) | ||||||
|     |> line([12.73, -0.09], %) |   |> line([12.73, -0.09], %) | ||||||
|     |> tangentialArcTo([24.95, -0.38], %) |   |> tangentialArcTo([24.95, -0.38], %) | ||||||
|     |> close(%) |   |> close(%) | ||||||
|     |> extrude(5, %)`) |   |> extrude(5, %)`) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   test.fixme( |   test.fixme( | ||||||
|     `Can use the import stdlib function on a local OBJ file`, |     `Can use the import stdlib function on a local OBJ file`, | ||||||
|     { tag: '@electron' }, |     { tag: '@electron' }, | ||||||
|     async ({ browserName }, testInfo) => { |     async ({ page, context }, testInfo) => { | ||||||
|       const { electronApp, page } = await setupElectron({ |       await context.folderSetupFn(async (dir) => { | ||||||
|         testInfo, |         const bracketDir = join(dir, 'cube') | ||||||
|         folderSetupFn: async (dir) => { |         await fsp.mkdir(bracketDir, { recursive: true }) | ||||||
|           const bracketDir = join(dir, 'cube') |         await fsp.copyFile( | ||||||
|           await fsp.mkdir(bracketDir, { recursive: true }) |           executorInputPath('cube.obj'), | ||||||
|           await fsp.copyFile( |           join(bracketDir, 'cube.obj') | ||||||
|             executorInputPath('cube.obj'), |         ) | ||||||
|             join(bracketDir, 'cube.obj') |         await fsp.writeFile(join(bracketDir, 'main.kcl'), '') | ||||||
|           ) |  | ||||||
|           await fsp.writeFile(join(bracketDir, 'main.kcl'), '') |  | ||||||
|         }, |  | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|       const viewportSize = { width: 1200, height: 500 } |       const viewportSize = { width: 1200, height: 500 } | ||||||
|       await page.setViewportSize(viewportSize) |       await page.setBodyDimensions(viewportSize) | ||||||
|  |  | ||||||
|       // Locators and constants |       // Locators and constants | ||||||
|       const u = await getUtils(page) |       const u = await getUtils(page) | ||||||
| @ -1115,8 +1188,6 @@ test.describe('Editor tests', () => { | |||||||
|           }) |           }) | ||||||
|           .toBeGreaterThan(15) |           .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' | import { expect } from '@playwright/test' | ||||||
|  |  | ||||||
| type CmdBarSerialised = | type CmdBarSerialised = | ||||||
| @ -26,9 +26,11 @@ type CmdBarSerialised = | |||||||
|  |  | ||||||
| export class CmdBarFixture { | export class CmdBarFixture { | ||||||
|   public page: Page |   public page: Page | ||||||
|  |   cmdBarOpenBtn!: Locator | ||||||
|  |  | ||||||
|   constructor(page: Page) { |   constructor(page: Page) { | ||||||
|     this.page = page |     this.page = page | ||||||
|  |     this.cmdBarOpenBtn = page.getByTestId('command-bar-open-button') | ||||||
|   } |   } | ||||||
|   reConstruct = (page: Page) => { |   reConstruct = (page: Page) => { | ||||||
|     this.page = page |     this.page = page | ||||||
| @ -116,4 +118,37 @@ export class CmdBarFixture { | |||||||
|       await this.page.keyboard.press('Enter') |       await this.page.keyboard.press('Enter') | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   openCmdBar = async (selectCmd?: 'promptToEdit') => { | ||||||
|  |     // TODO why does this button not work in electron tests? | ||||||
|  |     // await this.cmdBarOpenBtn.click() | ||||||
|  |     await this.page.keyboard.down('ControlOrMeta') | ||||||
|  |     await this.page.keyboard.press('KeyK') | ||||||
|  |     await this.page.keyboard.up('ControlOrMeta') | ||||||
|  |     await expect(this.page.getByPlaceholder('Search commands')).toBeVisible() | ||||||
|  |     if (selectCmd === 'promptToEdit') { | ||||||
|  |       const promptEditCommand = this.page.getByText( | ||||||
|  |         'Use Zoo AI to edit your kcl' | ||||||
|  |       ) | ||||||
|  |       await expect(promptEditCommand.first()).toBeVisible() | ||||||
|  |       await promptEditCommand.first().scrollIntoViewIfNeeded() | ||||||
|  |       await promptEditCommand.first().click() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get cmdSearchInput() { | ||||||
|  |     return this.page.getByTestId('cmd-bar-search') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get argumentInput() { | ||||||
|  |     return this.page.getByTestId('cmd-bar-arg-value') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get cmdOptions() { | ||||||
|  |     return this.page.getByTestId('cmd-bar-option') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   chooseCommand = async (commandName: string) => { | ||||||
|  |     await this.cmdOptions.getByText(commandName).click() | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ export class EditorFixture { | |||||||
|   private diagnosticsTooltip!: Locator |   private diagnosticsTooltip!: Locator | ||||||
|   private diagnosticsGutterIcon!: Locator |   private diagnosticsGutterIcon!: Locator | ||||||
|   private codeContent!: Locator |   private codeContent!: Locator | ||||||
|   private activeLine!: Locator |   public activeLine!: Locator | ||||||
|  |  | ||||||
|   constructor(page: Page) { |   constructor(page: Page) { | ||||||
|     this.page = page |     this.page = page | ||||||
| @ -29,7 +29,7 @@ export class EditorFixture { | |||||||
|   reConstruct = (page: Page) => { |   reConstruct = (page: Page) => { | ||||||
|     this.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.diagnosticsTooltip = page.locator('.cm-tooltip-lint') | ||||||
|     this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error') |     this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error') | ||||||
|     this.activeLine = this.page.locator('.cm-activeLine') |     this.activeLine = this.page.locator('.cm-activeLine') | ||||||
| @ -54,13 +54,13 @@ export class EditorFixture { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       if (!shouldNormalise) { |       if (!shouldNormalise) { | ||||||
|         const expectStart = expect(this.codeContent) |         const expectStart = expect.poll(() => this.codeContent.textContent()) | ||||||
|         if (not) { |         if (not) { | ||||||
|           const result = await expectStart.not.toContainText(code, { timeout }) |           const result = await expectStart.not.toContain(code) | ||||||
|           await resetPane() |           await resetPane() | ||||||
|           return result |           return result | ||||||
|         } |         } | ||||||
|         const result = await expectStart.toContainText(code, { timeout }) |         const result = await expectStart.toContain(code) | ||||||
|         await resetPane() |         await resetPane() | ||||||
|         return result |         return result | ||||||
|       } |       } | ||||||
| @ -147,4 +147,28 @@ export class EditorFixture { | |||||||
|   openPane() { |   openPane() { | ||||||
|     return openPane(this.page, this.paneButtonTestId) |     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 { | import type { | ||||||
|   BrowserContext, |   BrowserContext, | ||||||
|   ElectronApplication, |   ElectronApplication, | ||||||
|   Page, |  | ||||||
|   TestInfo, |   TestInfo, | ||||||
|  |   Page, | ||||||
| } from '@playwright/test' | } 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 fsp from 'fs/promises' | ||||||
| import { join } from 'path' | import { join } from 'path' | ||||||
| import { CmdBarFixture } from './cmdBarFixture' | import { CmdBarFixture } from './cmdBarFixture' | ||||||
| @ -20,11 +20,13 @@ export class AuthenticatedApp { | |||||||
|   public readonly page: Page |   public readonly page: Page | ||||||
|   public readonly context: BrowserContext |   public readonly context: BrowserContext | ||||||
|   public readonly testInfo: TestInfo |   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) { |   constructor(context: BrowserContext, page: Page, testInfo: TestInfo) { | ||||||
|     this.page = page |  | ||||||
|     this.context = context |     this.context = context | ||||||
|  |     this.page = page | ||||||
|     this.testInfo = testInfo |     this.testInfo = testInfo | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @ -49,9 +51,7 @@ export class AuthenticatedApp { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| interface Fixtures { | export interface Fixtures { | ||||||
|   app: AuthenticatedApp |  | ||||||
|   tronApp: AuthenticatedTronApp |  | ||||||
|   cmdBar: CmdBarFixture |   cmdBar: CmdBarFixture | ||||||
|   editor: EditorFixture |   editor: EditorFixture | ||||||
|   toolbar: ToolbarFixture |   toolbar: ToolbarFixture | ||||||
| @ -61,9 +61,11 @@ interface Fixtures { | |||||||
| export class AuthenticatedTronApp { | export class AuthenticatedTronApp { | ||||||
|   public readonly _page: Page |   public readonly _page: Page | ||||||
|   public page: Page |   public page: Page | ||||||
|   public readonly context: BrowserContext |   public context: BrowserContext | ||||||
|   public readonly testInfo: TestInfo |   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) { |   constructor(context: BrowserContext, page: Page, testInfo: TestInfo) { | ||||||
|     this._page = page |     this._page = page | ||||||
| @ -79,15 +81,22 @@ export class AuthenticatedTronApp { | |||||||
|       appSettings?: Partial<SaveSettingsPayload> |       appSettings?: Partial<SaveSettingsPayload> | ||||||
|     } = { fixtures: {} } |     } = { fixtures: {} } | ||||||
|   ) { |   ) { | ||||||
|     const { electronApp, page } = await setupElectron({ |     const { electronApp, page, context, dir } = await setupElectron({ | ||||||
|       testInfo: this.testInfo, |       testInfo: this.testInfo, | ||||||
|       folderSetupFn: arg.folderSetupFn, |       folderSetupFn: arg.folderSetupFn, | ||||||
|       cleanProjectDir: arg.cleanProjectDir, |       cleanProjectDir: arg.cleanProjectDir, | ||||||
|       appSettings: arg.appSettings, |       appSettings: arg.appSettings, | ||||||
|     }) |     }) | ||||||
|     this.page = page |     this.page = page | ||||||
|  |     this.context = context | ||||||
|     this.electronApp = electronApp |     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)) { |     for (const key of unsafeTypedKeys(arg.fixtures)) { | ||||||
|       const fixture = arg.fixtures[key] |       const fixture = arg.fixtures[key] | ||||||
| @ -110,32 +119,25 @@ export class AuthenticatedTronApp { | |||||||
|     }) |     }) | ||||||
| } | } | ||||||
|  |  | ||||||
| export const test = base.extend<Fixtures>({ | export const fixtures = { | ||||||
|   app: async ({ page, context }, use, testInfo) => { |   cmdBar: async ({ page }: { page: Page }, use: any) => { | ||||||
|     await use(new AuthenticatedApp(context, page, testInfo)) |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|   }, |  | ||||||
|   tronApp: async ({ page, context }, use, testInfo) => { |  | ||||||
|     await use(new AuthenticatedTronApp(context, page, testInfo)) |  | ||||||
|   }, |  | ||||||
|   cmdBar: async ({ page }, use) => { |  | ||||||
|     await use(new CmdBarFixture(page)) |     await use(new CmdBarFixture(page)) | ||||||
|   }, |   }, | ||||||
|   editor: async ({ page }, use) => { |   editor: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new EditorFixture(page)) |     await use(new EditorFixture(page)) | ||||||
|   }, |   }, | ||||||
|   toolbar: async ({ page }, use) => { |   toolbar: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new ToolbarFixture(page)) |     await use(new ToolbarFixture(page)) | ||||||
|   }, |   }, | ||||||
|   scene: async ({ page }, use) => { |   scene: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new SceneFixture(page)) |     await use(new SceneFixture(page)) | ||||||
|   }, |   }, | ||||||
|   homePage: async ({ page }, use) => { |   homePage: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new HomePageFixture(page)) |     await use(new HomePageFixture(page)) | ||||||
|   }, |   }, | ||||||
| }) | } | ||||||
|  |  | ||||||
| test.afterEach(async ({ page }, testInfo) => { |  | ||||||
|   await tearDown(page, testInfo) |  | ||||||
| }) |  | ||||||
|  |  | ||||||
| export { expect } from '@playwright/test' |  | ||||||
|  | |||||||
| @ -14,10 +14,14 @@ interface HomePageState { | |||||||
| export class HomePageFixture { | export class HomePageFixture { | ||||||
|   public page: Page |   public page: Page | ||||||
|  |  | ||||||
|  |   projectSection!: Locator | ||||||
|   projectCard!: Locator |   projectCard!: Locator | ||||||
|   projectCardTitle!: Locator |   projectCardTitle!: Locator | ||||||
|   projectCardFile!: Locator |   projectCardFile!: Locator | ||||||
|   projectCardFolder!: Locator |   projectCardFolder!: Locator | ||||||
|  |   projectButtonNew!: Locator | ||||||
|  |   projectButtonContinue!: Locator | ||||||
|  |   projectTextName!: Locator | ||||||
|   sortByDateBtn!: Locator |   sortByDateBtn!: Locator | ||||||
|   sortByNameBtn!: Locator |   sortByNameBtn!: Locator | ||||||
|  |  | ||||||
| @ -28,11 +32,19 @@ export class HomePageFixture { | |||||||
|   reConstruct = (page: Page) => { |   reConstruct = (page: Page) => { | ||||||
|     this.page = page |     this.page = page | ||||||
|  |  | ||||||
|  |     this.projectSection = this.page.getByTestId('home-section') | ||||||
|  |  | ||||||
|     this.projectCard = this.page.getByTestId('project-link') |     this.projectCard = this.page.getByTestId('project-link') | ||||||
|     this.projectCardTitle = this.page.getByTestId('project-title') |     this.projectCardTitle = this.page.getByTestId('project-title') | ||||||
|     this.projectCardFile = this.page.getByTestId('project-file-count') |     this.projectCardFile = this.page.getByTestId('project-file-count') | ||||||
|     this.projectCardFolder = this.page.getByTestId('project-folder-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.sortByDateBtn = this.page.getByTestId('home-sort-by-modified') | ||||||
|     this.sortByNameBtn = this.page.getByTestId('home-sort-by-name') |     this.sortByNameBtn = this.page.getByTestId('home-sort-by-name') | ||||||
|   } |   } | ||||||
| @ -91,10 +103,25 @@ export class HomePageFixture { | |||||||
|       .toEqual(expectedState) |       .toEqual(expectedState) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   createAndGoToProject = async (projectTitle = 'project-$nnn') => { | ||||||
|  |     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) => { |   openProject = async (projectTitle: string) => { | ||||||
|     const projectCard = this.projectCard.locator( |     const projectCard = this.projectCard.locator( | ||||||
|       this.page.getByText(projectTitle) |       this.page.getByText(projectTitle) | ||||||
|     ) |     ) | ||||||
|     await projectCard.click() |     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) | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -36,7 +36,8 @@ type DragFromHandler = ( | |||||||
|  |  | ||||||
| export class SceneFixture { | export class SceneFixture { | ||||||
|   public page: Page |   public page: Page | ||||||
|  |   public streamWrapper!: Locator | ||||||
|  |   public loadingIndicator!: Locator | ||||||
|   private exeIndicator!: Locator |   private exeIndicator!: Locator | ||||||
|  |  | ||||||
|   constructor(page: Page) { |   constructor(page: Page) { | ||||||
| @ -53,8 +54,9 @@ export class SceneFixture { | |||||||
|  |  | ||||||
|   expectState = async (expected: SceneSerialised) => { |   expectState = async (expected: SceneSerialised) => { | ||||||
|     return expect |     return expect | ||||||
|       .poll(() => this._serialiseScene(), { |       .poll(async () => await this._serialiseScene(), { | ||||||
|         message: `Expected scene state to match`, |         intervals: [1_000, 2_000, 10_000], | ||||||
|  |         timeout: 60000, | ||||||
|       }) |       }) | ||||||
|       .toEqual(expected) |       .toEqual(expected) | ||||||
|   } |   } | ||||||
| @ -63,6 +65,8 @@ export class SceneFixture { | |||||||
|     this.page = page |     this.page = page | ||||||
|  |  | ||||||
|     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') |     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') | ||||||
|  |     this.streamWrapper = page.getByTestId('stream') | ||||||
|  |     this.loadingIndicator = this.streamWrapper.getByTestId('loading') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   makeMouseHelpers = ( |   makeMouseHelpers = ( | ||||||
| @ -187,7 +191,10 @@ export class SceneFixture { | |||||||
|         type: 'default_camera_get_settings', |         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([ |     const position = await Promise.all([ | ||||||
|       this.page.getByTestId('cam-x-position').inputValue().then(Number), |       this.page.getByTestId('cam-x-position').inputValue().then(Number), | ||||||
|       this.page.getByTestId('cam-y-position').inputValue().then(Number), |       this.page.getByTestId('cam-y-position').inputValue().then(Number), | ||||||
| @ -222,6 +229,7 @@ export class SceneFixture { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async clickGizmoMenuItem(name: string) { |   async clickGizmoMenuItem(name: string) { | ||||||
|  |     await this.gizmo.hover() | ||||||
|     await this.gizmo.click({ button: 'right' }) |     await this.gizmo.click({ button: 'right' }) | ||||||
|     const buttonToTest = this.page.getByRole('button', { |     const buttonToTest = this.page.getByRole('button', { | ||||||
|       name: name, |       name: name, | ||||||
|  | |||||||
| @ -1,12 +1,22 @@ | |||||||
| import type { Page, Locator } from '@playwright/test' | import type { Page, Locator } from '@playwright/test' | ||||||
| import { expect } from './fixtureSetup' | import { expect } from '../zoo-test' | ||||||
| import { doAndWaitForImageDiff } from '../test-utils' | import { | ||||||
|  |   checkIfPaneIsOpen, | ||||||
|  |   closePane, | ||||||
|  |   doAndWaitForImageDiff, | ||||||
|  |   openPane, | ||||||
|  | } from '../test-utils' | ||||||
|  | import { SidebarType } from 'components/ModelingSidebar/ModelingPanes' | ||||||
|  | import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants' | ||||||
|  |  | ||||||
| export class ToolbarFixture { | export class ToolbarFixture { | ||||||
|   public page: Page |   public page: Page | ||||||
|  |  | ||||||
|   extrudeButton!: Locator |   extrudeButton!: Locator | ||||||
|   loftButton!: Locator |   loftButton!: Locator | ||||||
|  |   sweepButton!: Locator | ||||||
|  |   filletButton!: Locator | ||||||
|  |   chamferButton!: Locator | ||||||
|   shellButton!: Locator |   shellButton!: Locator | ||||||
|   offsetPlaneButton!: Locator |   offsetPlaneButton!: Locator | ||||||
|   startSketchBtn!: Locator |   startSketchBtn!: Locator | ||||||
| @ -20,6 +30,10 @@ export class ToolbarFixture { | |||||||
|   filePane!: Locator |   filePane!: Locator | ||||||
|   exeIndicator!: Locator |   exeIndicator!: Locator | ||||||
|   treeInputField!: Locator |   treeInputField!: Locator | ||||||
|  |   /** The sidebar button for the Feature Tree pane */ | ||||||
|  |   featureTreeId = 'feature-tree' as const | ||||||
|  |   /** The pane element for the Feature Tree */ | ||||||
|  |   featureTreePane!: Locator | ||||||
|  |  | ||||||
|   constructor(page: Page) { |   constructor(page: Page) { | ||||||
|     this.page = page |     this.page = page | ||||||
| @ -29,6 +43,9 @@ export class ToolbarFixture { | |||||||
|     this.page = page |     this.page = page | ||||||
|     this.extrudeButton = page.getByTestId('extrude') |     this.extrudeButton = page.getByTestId('extrude') | ||||||
|     this.loftButton = page.getByTestId('loft') |     this.loftButton = page.getByTestId('loft') | ||||||
|  |     this.sweepButton = page.getByTestId('sweep') | ||||||
|  |     this.filletButton = page.getByTestId('fillet3d') | ||||||
|  |     this.chamferButton = page.getByTestId('chamfer3d') | ||||||
|     this.shellButton = page.getByTestId('shell') |     this.shellButton = page.getByTestId('shell') | ||||||
|     this.offsetPlaneButton = page.getByTestId('plane-offset') |     this.offsetPlaneButton = page.getByTestId('plane-offset') | ||||||
|     this.startSketchBtn = page.getByTestId('sketch') |     this.startSketchBtn = page.getByTestId('sketch') | ||||||
| @ -41,10 +58,15 @@ export class ToolbarFixture { | |||||||
|     this.treeInputField = page.getByTestId('tree-input-field') |     this.treeInputField = page.getByTestId('tree-input-field') | ||||||
|  |  | ||||||
|     this.filePane = page.locator('#files-pane') |     this.filePane = page.locator('#files-pane') | ||||||
|  |     this.featureTreePane = page.locator('#feature-tree-pane') | ||||||
|     this.fileCreateToast = page.getByText('Successfully created') |     this.fileCreateToast = page.getByText('Successfully created') | ||||||
|     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') |     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get logoLink() { | ||||||
|  |     return this.page.getByTestId('app-logo') | ||||||
|  |   } | ||||||
|  |  | ||||||
|   startSketchPlaneSelection = async () => |   startSketchPlaneSelection = async () => | ||||||
|     doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500) |     doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500) | ||||||
|  |  | ||||||
| @ -91,4 +113,76 @@ export class ToolbarFixture { | |||||||
|       await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 }) |       await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 }) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   async closePane(paneId: SidebarType) { | ||||||
|  |     return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) | ||||||
|  |   } | ||||||
|  |   async openPane(paneId: SidebarType) { | ||||||
|  |     return openPane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) | ||||||
|  |   } | ||||||
|  |   async checkIfPaneIsOpen(paneId: SidebarType) { | ||||||
|  |     return checkIfPaneIsOpen(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async openFeatureTreePane() { | ||||||
|  |     return this.openPane(this.featureTreeId) | ||||||
|  |   } | ||||||
|  |   async closeFeatureTreePane() { | ||||||
|  |     await this.closePane(this.featureTreeId) | ||||||
|  |   } | ||||||
|  |   async checkIfFeatureTreePaneIsOpen() { | ||||||
|  |     return this.checkIfPaneIsOpen(this.featureTreeId) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get a specific operation button from the Feature Tree pane | ||||||
|  |    */ | ||||||
|  |   async getFeatureTreeOperation(operationName: string, operationIndex: number) { | ||||||
|  |     await this.openFeatureTreePane() | ||||||
|  |     await expect(this.featureTreePane).toBeVisible() | ||||||
|  |     return this.featureTreePane | ||||||
|  |       .getByRole('button', { | ||||||
|  |         name: operationName, | ||||||
|  |       }) | ||||||
|  |       .nth(operationIndex) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * View source on a specific operation in the Feature Tree pane. | ||||||
|  |    * @param operationName The name of the operation type | ||||||
|  |    * @param operationIndex The index out of operations of this type | ||||||
|  |    */ | ||||||
|  |   async viewSourceOnOperation(operationName: string, operationIndex: number) { | ||||||
|  |     const operationButton = await this.getFeatureTreeOperation( | ||||||
|  |       operationName, | ||||||
|  |       operationIndex | ||||||
|  |     ) | ||||||
|  |     const viewSourceMenuButton = this.page.getByRole('button', { | ||||||
|  |       name: 'View KCL source code', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     await operationButton.click({ button: 'right' }) | ||||||
|  |     await expect(viewSourceMenuButton).toBeVisible() | ||||||
|  |     await viewSourceMenuButton.click() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Go to definition on a specific operation in the Feature Tree pane | ||||||
|  |    */ | ||||||
|  |   async goToDefinitionOnOperation( | ||||||
|  |     operationName: string, | ||||||
|  |     operationIndex: number | ||||||
|  |   ) { | ||||||
|  |     const operationButton = await this.getFeatureTreeOperation( | ||||||
|  |       operationName, | ||||||
|  |       operationIndex | ||||||
|  |     ) | ||||||
|  |     const goToDefinitionMenuButton = this.page.getByRole('button', { | ||||||
|  |       name: 'View function definition', | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     await operationButton.click({ button: 'right' }) | ||||||
|  |     await expect(goToDefinitionMenuButton).toBeVisible() | ||||||
|  |     await goToDefinitionMenuButton.click() | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,29 +1,22 @@ | |||||||
| import { test, expect } from '@playwright/test' | import { test, expect } from './zoo-test' | ||||||
| import { setupElectron, tearDown, executorInputPath } from './test-utils' | import { executorInputPath } from './test-utils' | ||||||
| import { join } from 'path' | import { join } from 'path' | ||||||
| import fsp from 'fs/promises' | import fsp from 'fs/promises' | ||||||
|  |  | ||||||
| test.afterEach(async ({ page }, testInfo) => { |  | ||||||
|   await tearDown(page, testInfo) |  | ||||||
| }) |  | ||||||
|  |  | ||||||
| test( | test( | ||||||
|   'When machine-api server not found butt is disabled and shows the reason', |   'When machine-api server not found butt is disabled and shows the reason', | ||||||
|   { tag: '@electron' }, |   { tag: '@electron' }, | ||||||
|   async ({ browserName }, testInfo) => { |   async ({ context, page }, testInfo) => { | ||||||
|     const { electronApp, page } = await setupElectron({ |     await context.folderSetupFn(async (dir) => { | ||||||
|       testInfo, |       const bracketDir = join(dir, 'bracket') | ||||||
|       folderSetupFn: async (dir) => { |       await fsp.mkdir(bracketDir, { recursive: true }) | ||||||
|         const bracketDir = join(dir, 'bracket') |       await fsp.copyFile( | ||||||
|         await fsp.mkdir(bracketDir, { recursive: true }) |         executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||||
|         await fsp.copyFile( |         join(bracketDir, 'main.kcl') | ||||||
|           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() |     await expect(page.getByText('bracket')).toBeVisible() | ||||||
|  |  | ||||||
| @ -47,28 +40,23 @@ test( | |||||||
|     // that the machine-api server is not found |     // that the machine-api server is not found | ||||||
|     await makeButton.hover() |     await makeButton.hover() | ||||||
|     await expect(page.getByText(notFoundText).first()).toBeVisible() |     await expect(page.getByText(notFoundText).first()).toBeVisible() | ||||||
|  |  | ||||||
|     await electronApp.close() |  | ||||||
|   } |   } | ||||||
| ) | ) | ||||||
|  |  | ||||||
| test( | test( | ||||||
|   'When machine-api server not found home screen & project status shows the reason', |   'When machine-api server not found home screen & project status shows the reason', | ||||||
|   { tag: '@electron' }, |   { tag: '@electron' }, | ||||||
|   async ({ browserName }, testInfo) => { |   async ({ context, page }, testInfo) => { | ||||||
|     const { electronApp, page } = await setupElectron({ |     await context.folderSetupFn(async (dir) => { | ||||||
|       testInfo, |       const bracketDir = join(dir, 'bracket') | ||||||
|       folderSetupFn: async (dir) => { |       await fsp.mkdir(bracketDir, { recursive: true }) | ||||||
|         const bracketDir = join(dir, 'bracket') |       await fsp.copyFile( | ||||||
|         await fsp.mkdir(bracketDir, { recursive: true }) |         executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||||
|         await fsp.copyFile( |         join(bracketDir, 'main.kcl') | ||||||
|           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' |     const notFoundText = 'Machine API server was not discovered' | ||||||
|  |  | ||||||
| @ -91,7 +79,5 @@ test( | |||||||
|  |  | ||||||
|     await networkMachineToggle.hover() |     await networkMachineToggle.hover() | ||||||
|     await expect(page.getByText(notFoundText).nth(1)).toBeVisible() |     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,86 +1,78 @@ | |||||||
| import { test, expect } from '@playwright/test' | import { test, expect } from './zoo-test' | ||||||
| import { join } from 'path' | import { join } from 'path' | ||||||
| import fsp from 'fs/promises' | import fsp from 'fs/promises' | ||||||
| import { | import { getUtils, executorInputPath, createProject } from './test-utils' | ||||||
|   getUtils, |  | ||||||
|   setup, |  | ||||||
|   setupElectron, |  | ||||||
|   tearDown, |  | ||||||
|   executorInputPath, |  | ||||||
|   createProject, |  | ||||||
| } from './test-utils' |  | ||||||
| import { bracket } from 'lib/exampleKcl' | import { bracket } from 'lib/exampleKcl' | ||||||
| import { onboardingPaths } from 'routes/Onboarding/paths' | import { onboardingPaths } from 'routes/Onboarding/paths' | ||||||
| import { | import { | ||||||
|   TEST_SETTINGS_KEY, |   TEST_SETTINGS_KEY, | ||||||
|   TEST_SETTINGS_ONBOARDING_START, |   TEST_SETTINGS_ONBOARDING_START, | ||||||
|   TEST_SETTINGS_ONBOARDING_EXPORT, |   TEST_SETTINGS_ONBOARDING_EXPORT, | ||||||
|   TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING, |  | ||||||
|   TEST_SETTINGS_ONBOARDING_USER_MENU, |   TEST_SETTINGS_ONBOARDING_USER_MENU, | ||||||
| } from './storageStates' | } from './storageStates' | ||||||
| import * as TOML from '@iarna/toml' | import * as TOML from '@iarna/toml' | ||||||
| import { expectPixelColor } from './fixtures/sceneFixture' | import { expectPixelColor } from './fixtures/sceneFixture' | ||||||
|  |  | ||||||
| test.beforeEach(async ({ context, page }, testInfo) => { | // Because onboarding relies on an app setting we need to set it as incompletel | ||||||
|   if (testInfo.tags.includes('@electron')) { | // for all these tests. | ||||||
|     return |  | ||||||
|   } |  | ||||||
|   await setup(context, page) |  | ||||||
| }) |  | ||||||
|  |  | ||||||
| test.afterEach(async ({ page }, testInfo) => { |  | ||||||
|   await tearDown(page, testInfo) |  | ||||||
| }) |  | ||||||
|  |  | ||||||
| test.describe('Onboarding tests', () => { | test.describe('Onboarding tests', () => { | ||||||
|   test('Onboarding code is shown in the editor', async ({ page }) => { |   test( | ||||||
|     const u = await getUtils(page) |     'Onboarding code is shown in the editor', | ||||||
|  |     { | ||||||
|     // Override beforeEach test setup |       appSettings: { | ||||||
|     await page.addInitScript( |         app: { | ||||||
|       async ({ settingsKey }) => { |           onboardingStatus: 'incomplete', | ||||||
|         // Give no initial code, so that the onboarding start is shown immediately |         }, | ||||||
|         localStorage.removeItem('persistCode') |  | ||||||
|         localStorage.removeItem(settingsKey) |  | ||||||
|       }, |       }, | ||||||
|       { 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: 1000 }) |       // 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 |       // *and* that the code is shown in the editor | ||||||
|     await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible() |       await expect(page.locator('.cm-content')).toContainText( | ||||||
|  |         '// Shelf Bracket' | ||||||
|  |       ) | ||||||
|  |  | ||||||
|     // *and* that the code is shown in the editor |       // Make sure the model loaded | ||||||
|     await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket') |       const XYPlanePoint = { x: 774, y: 116 } as const | ||||||
|  |       const modelColor: [number, number, number] = [45, 45, 45] | ||||||
|     // Make sure the model loaded |       await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) | ||||||
|     const XYPlanePoint = { x: 774, y: 116 } as const |       expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan( | ||||||
|     const modelColor: [number, number, number] = [45, 45, 45] |         8 | ||||||
|     await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) |       ) | ||||||
|     expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8) |     } | ||||||
|   }) |   ) | ||||||
|  |  | ||||||
|   test( |   test( | ||||||
|     'Desktop: fresh onboarding executes and loads', |     'Desktop: fresh onboarding executes and loads', | ||||||
|     { tag: '@electron' }, |     { | ||||||
|     async ({ browserName: _ }, testInfo) => { |       tag: '@electron', | ||||||
|       const { electronApp, page } = await setupElectron({ |       appSettings: { | ||||||
|         testInfo, |         app: { | ||||||
|         appSettings: { |           onboardingStatus: 'incomplete', | ||||||
|           app: { |  | ||||||
|             onboardingStatus: 'incomplete', |  | ||||||
|           }, |  | ||||||
|         }, |         }, | ||||||
|         cleanProjectDir: true, |       }, | ||||||
|       }) |       cleanProjectDir: true, | ||||||
|  |     }, | ||||||
|  |     async ({ page, homePage }, testInfo) => { | ||||||
|       const u = await getUtils(page) |       const u = await getUtils(page) | ||||||
|  |  | ||||||
|       const viewportSize = { width: 1200, height: 1000 } |       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 test.step(`Create a project and open to the onboarding`, async () => { | ||||||
|         await createProject({ name: 'project-link', page }) |         await createProject({ name: 'project-link', page }) | ||||||
| @ -108,337 +100,361 @@ test.describe('Onboarding tests', () => { | |||||||
|  |  | ||||||
|         //await expectPixelColor(page, modelColor, XYPlanePoint, 8) |         //await expectPixelColor(page, modelColor, XYPlanePoint, 8) | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|       await electronApp.close() |  | ||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|   test('Code resets after confirmation', async ({ page }) => { |   test( | ||||||
|     const initialCode = `sketch001 = startSketchOn('XZ')` |     'Code resets after confirmation', | ||||||
|  |     { | ||||||
|     // Load the page up with some code so we see the confirmation warning |       appSettings: { | ||||||
|     // when we go to replay onboarding |         app: { | ||||||
|     await page.addInitScript((code) => { |           onboardingStatus: 'incomplete', | ||||||
|       localStorage.setItem('persistCode', code) |         }, | ||||||
|     }, initialCode) |  | ||||||
|  |  | ||||||
|     const u = await getUtils(page) |  | ||||||
|     await page.setViewportSize({ width: 1200, height: 1000 }) |  | ||||||
|     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') |  | ||||||
|  |  | ||||||
|     // 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('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) |  | ||||||
|       }, |       }, | ||||||
|       { |       cleanProjectDir: true, | ||||||
|         settingsKey: TEST_SETTINGS_KEY, |     }, | ||||||
|         settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_START }), |     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 |       // Replay the onboarding | ||||||
|     await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible() |       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') { |       const nextButton = page.getByTestId('onboarding-next') | ||||||
|       await expect(nextButton).toBeVisible() |       await nextButton.hover() | ||||||
|       await nextButton.click() |       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 |   test( | ||||||
|     await expect(nextButton).toBeVisible() |     'Click through each onboarding step', | ||||||
|     await nextButton.click() |     { | ||||||
|  |       appSettings: { | ||||||
|     // Test that the onboarding pane is gone |         app: { | ||||||
|     await expect(page.getByTestId('onboarding-content')).not.toBeVisible() |           onboardingStatus: 'incomplete', | ||||||
|     await expect(page.url()).not.toContain('onboarding') |         }, | ||||||
|  |  | ||||||
|     await u.openAndClearDebugPanel() |  | ||||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') |  | ||||||
|     await u.closeDebugPanel() |  | ||||||
|  |  | ||||||
|     // TODO: jess to fix |  | ||||||
|     // Make sure the model loaded |  | ||||||
|     //const XYPlanePoint = { x: 774, y: 516 } as const |  | ||||||
|     // const modelColor: [number, number, number] = [129, 129, 129] |  | ||||||
|     // await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) |  | ||||||
|     // await expectPixelColor(page, modelColor, XYPlanePoint, 20) |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   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) |  | ||||||
|       }, |       }, | ||||||
|       { |     }, | ||||||
|         settingsKey: TEST_SETTINGS_KEY, |     async ({ context, page, homePage }) => { | ||||||
|         settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_EXPORT }), |       // 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 onboarding pane is gone | ||||||
|  |       await expect(page.getByTestId('onboarding-content')).not.toBeVisible() | ||||||
|     // Test that the redirect happened |       await expect.poll(() => page.url()).not.toContain('/onboarding') | ||||||
|     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( | ||||||
|  |     '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.fixme( | test.fixme( | ||||||
|   'Restarting onboarding on desktop takes one attempt', |   'Restarting onboarding on desktop takes one attempt', | ||||||
|   { tag: '@electron' }, |   { | ||||||
|   async ({ browser: _ }, testInfo) => { |     appSettings: { | ||||||
|     const { electronApp, page } = await setupElectron({ |       app: { | ||||||
|       testInfo, |         onboardingStatus: 'dismissed', | ||||||
|       folderSetupFn: async (dir) => { |  | ||||||
|         const routerTemplateDir = join(dir, 'router-template-slate') |  | ||||||
|         await fsp.mkdir(routerTemplateDir, { recursive: true }) |  | ||||||
|         await fsp.copyFile( |  | ||||||
|           executorInputPath('router-template-slate.kcl'), |  | ||||||
|           join(routerTemplateDir, 'main.kcl') |  | ||||||
|         ) |  | ||||||
|       }, |       }, | ||||||
|  |     }, | ||||||
|  |     cleanProjectDir: true, | ||||||
|  |   }, | ||||||
|  |   async ({ context, page, homePage }, testInfo) => { | ||||||
|  |     await context.folderSetupFn(async (dir) => { | ||||||
|  |       const routerTemplateDir = join(dir, 'router-template-slate') | ||||||
|  |       await fsp.mkdir(routerTemplateDir, { recursive: true }) | ||||||
|  |       await fsp.copyFile( | ||||||
|  |         executorInputPath('router-template-slate.kcl'), | ||||||
|  |         join(routerTemplateDir, 'main.kcl') | ||||||
|  |       ) | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     // Our constants |     // Our constants | ||||||
| @ -450,9 +466,8 @@ test.fixme( | |||||||
|     const restartOnboardingButton = page.getByRole('button', { |     const restartOnboardingButton = page.getByRole('button', { | ||||||
|       name: 'Reset onboarding', |       name: 'Reset onboarding', | ||||||
|     }) |     }) | ||||||
|     const restartConfirmationButton = page.getByRole('button', { |     const nextButton = page.getByTestId('onboarding-next') | ||||||
|       name: 'Make a new project', |  | ||||||
|     }) |  | ||||||
|     const tutorialProjectIndicator = page |     const tutorialProjectIndicator = page | ||||||
|       .getByTestId('project-sidebar-toggle') |       .getByTestId('project-sidebar-toggle') | ||||||
|       .filter({ hasText: 'Tutorial Project 00' }) |       .filter({ hasText: 'Tutorial Project 00' }) | ||||||
| @ -471,7 +486,7 @@ test.fixme( | |||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await test.step('Navigate into project', async () => { |     await test.step('Navigate into project', async () => { | ||||||
|       await page.setViewportSize({ width: 1200, height: 1000 }) |       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|  |  | ||||||
|       page.on('console', console.log) |       page.on('console', console.log) | ||||||
|  |  | ||||||
| @ -487,8 +502,8 @@ test.fixme( | |||||||
|       await helpMenuButton.click() |       await helpMenuButton.click() | ||||||
|       await restartOnboardingButton.click() |       await restartOnboardingButton.click() | ||||||
|  |  | ||||||
|       await expect(restartConfirmationButton).toBeVisible() |       await nextButton.hover() | ||||||
|       await restartConfirmationButton.click() |       await nextButton.click() | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await test.step('Confirm that the onboarding has restarted', async () => { |     await test.step('Confirm that the onboarding has restarted', async () => { | ||||||
| @ -520,11 +535,9 @@ test.fixme( | |||||||
|  |  | ||||||
|       await restartOnboardingSettingsButton.click() |       await restartOnboardingSettingsButton.click() | ||||||
|       // Since the code is empty, we should not see the confirmation dialog |       // 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(tutorialProjectIndicator).toBeVisible() | ||||||
|       await expect(tutorialModalText).toBeVisible() |       await expect(tutorialModalText).toBeVisible() | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     await electronApp.close() |  | ||||||
|   } |   } | ||||||
| ) | ) | ||||||
|  | |||||||
							
								
								
									
										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 { test, expect, Page } from './zoo-test' | ||||||
| import { join } from 'path' | import path from 'path' | ||||||
| import * as fsp from 'fs/promises' | import * as fsp from 'fs/promises' | ||||||
| import { | import { getUtils, executorInputPath } from './test-utils' | ||||||
|   getUtils, |  | ||||||
|   setup, |  | ||||||
|   setupElectron, |  | ||||||
|   tearDown, |  | ||||||
|   executorInputPath, |  | ||||||
| } from './test-utils' |  | ||||||
| import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates' | import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates' | ||||||
| import { bracket } from 'lib/exampleKcl' | 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', () => { | test.describe('Regression tests', () => { | ||||||
|   // bugs we found that don't fit neatly into other categories |   // 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 |     // 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 |     // 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 |     // Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|     await page.addInitScript(async () => { |     await context.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `sketch2 = startSketchOn("XY") |         `sketch2 = startSketchOn("XY") | ||||||
| sketch001 = startSketchAt([-0, -0]) |   sketch001 = startSketchAt([-0, -0]) | ||||||
|   |> line([0, 0], %) |     |> line([0, 0], %) | ||||||
|   |> line([-4.84, -5.29], %) |     |> line([-4.84, -5.29], %) | ||||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) |     |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||||
|   |> close(%)` |     |> 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 |     // error in guter | ||||||
|     await expect(page.locator('.cm-lint-marker-error')).toBeVisible() |     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 ({ |   test('user should not have to press down twice in cmdbar', async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     // because the model has `line([0,0]..` it is valid code, but the model is invalid |     // 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 |     // regression test for https://github.com/KittyCAD/modeling-app/issues/3251 | ||||||
| @ -64,26 +56,38 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|     await page.addInitScript(async () => { |     await page.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `sketch2 = startSketchOn("XY") |         `sketch001 = startSketchOn('XY') | ||||||
| sketch001 = startSketchAt([-0, -0]) |   |> startProfileAt([82.33, 238.21], %) | ||||||
|   |> line([0, 0], %) |   |> angledLine([0, 288.63], %, $rectangleSegmentA001) | ||||||
|   |> line([-4.84, -5.29], %) |   |> angledLine([ | ||||||
|  |        segAng(rectangleSegmentA001) - 90, | ||||||
|  |        197.97 | ||||||
|  |      ], %, $rectangleSegmentB001) | ||||||
|  |   |> angledLine([ | ||||||
|  |        segAng(rectangleSegmentA001), | ||||||
|  |        -segLen(rectangleSegmentA001) | ||||||
|  |      ], %, $rectangleSegmentC001) | ||||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) |   |> 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 u.waitForPageLoad() | ||||||
|  |  | ||||||
|     await test.step('Check arrow down works', async () => { |     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.getByTestId('command-bar-open-button').click() | ||||||
|  |  | ||||||
|       await page |       const floppy = page.getByRole('option', { | ||||||
|         .getByRole('option', { name: 'floppy disk arrow Export' }) |         name: 'floppy disk arrow Export', | ||||||
|         .click() |       }) | ||||||
|  |  | ||||||
|  |       await floppy.click() | ||||||
|  |  | ||||||
|       // press arrow down key twice |       // press arrow down key twice | ||||||
|       await page.keyboard.press('ArrowDown') |       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) |     const u = await getUtils(page) | ||||||
|     await page.addInitScript(async () => { |     await page.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `sketch001 = startSketchOn('-XZ') |         `sketch001 = startSketchOn('-XZ') | ||||||
|     |> startProfileAt([-6.95, 4.98], %) |   |> startProfileAt([-6.95, 4.98], %) | ||||||
|     |> line([25.1, 0.41], %) |   |> line([25.1, 0.41], %) | ||||||
|     |> line([0.73, -14.93], %) |   |> line([0.73, -14.93], %) | ||||||
|     |> line([-23.44, 0.52], %)` |   |> 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 |     // expand variables section | ||||||
|     const variablesTabButton = page.getByTestId('variables-pane-button') |     const variablesTabButton = page.getByTestId('variables-pane-button') | ||||||
| @ -148,14 +153,15 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|     ).toBeVisible() |     ).toBeVisible() | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   test('re-executes', async ({ page }) => { |   test('re-executes', async ({ page, homePage }) => { | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|     await page.addInitScript(async () => { |     await page.addInitScript(async () => { | ||||||
|       localStorage.setItem('persistCode', `myVar = 5`) |       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') |     const variablesTabButton = page.getByTestId('variables-pane-button') | ||||||
|     await variablesTabButton.click() |     await variablesTabButton.click() | ||||||
| @ -174,32 +180,33 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|       page.locator('.pretty-json-container >> text=myVar:67') |       page.locator('.pretty-json-container >> text=myVar:67') | ||||||
|     ).toBeVisible() |     ).toBeVisible() | ||||||
|   }) |   }) | ||||||
|   test('ProgramMemory can be serialised', async ({ page }) => { |   test('ProgramMemory can be serialised', async ({ page, homePage }) => { | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|     await page.addInitScript(async () => { |     await page.addInitScript(async () => { | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         'persistCode', |         'persistCode', | ||||||
|         `part = startSketchOn('XY') |         `part = startSketchOn('XY') | ||||||
|     |> startProfileAt([0, 0], %) |   |> startProfileAt([0, 0], %) | ||||||
|     |> line([0, 1], %) |   |> line([0, 1], %) | ||||||
|     |> line([1, 0], %) |   |> line([1, 0], %) | ||||||
|     |> line([0, -1], %) |   |> line([0, -1], %) | ||||||
|     |> close(%) |   |> close(%) | ||||||
|     |> extrude(1, %) |   |> extrude(1, %) | ||||||
|     |> patternLinear3d({ |   |> patternLinear3d({ | ||||||
|           axis: [1, 0, 1], |         axis: [1, 0, 1], | ||||||
|           repetitions: 3, |         repetitions: 3, | ||||||
|           distance: 6 |         distance: 6 | ||||||
|         }, %)` |       }, %)` | ||||||
|       ) |       ) | ||||||
|     }) |     }) | ||||||
|     await page.setViewportSize({ width: 1000, height: 500 }) |     await page.setBodyDimensions({ width: 1000, height: 500 }) | ||||||
|  |  | ||||||
|     const messages: string[] = [] |     const messages: string[] = [] | ||||||
|  |  | ||||||
|     // Listen for all console events and push the message text to an array |     // Listen for all console events and push the message text to an array | ||||||
|     page.on('console', (message) => messages.push(message.text())) |     page.on('console', (message) => messages.push(message.text())) | ||||||
|     await u.waitForAuthSkipAppStart() |     await homePage.goToModelingScene() | ||||||
|  |     await u.waitForPageLoad() | ||||||
|  |  | ||||||
|     // wait for execution done |     // wait for execution done | ||||||
|     await u.openDebugPanel() |     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) |     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() | ||||||
|  |     await u.waitForPageLoad() | ||||||
|  |  | ||||||
|     const zooLogo = page.locator('[data-testid="app-logo"]') |     const zooLogo = page.locator('[data-testid="app-logo"]') | ||||||
|     // Make sure it's not a link |     // Make sure it's not a link | ||||||
|     await expect(zooLogo).not.toHaveAttribute('href') |     await expect(zooLogo).not.toHaveAttribute('href') | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   test( |   test( | ||||||
|     'Position _ Is Out Of Range... regression test', |     'Position _ Is Out Of Range... regression test', | ||||||
|     { tag: ['@skipWin'] }, |     { tag: ['@skipWin'] }, | ||||||
|     async ({ page }) => { |     async ({ context, page, homePage }) => { | ||||||
|       // SKip on windows, its being weird. |       // SKip on windows, its being weird. | ||||||
|       test.skip( |       test.skip( | ||||||
|         process.platform === 'win32', |         process.platform === 'win32', | ||||||
| @ -233,25 +247,26 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|  |  | ||||||
|       const u = await getUtils(page) |       const u = await getUtils(page) | ||||||
|       // const PUR = 400 / 37.5 //pixeltoUnitRatio |       // const PUR = 400 / 37.5 //pixeltoUnitRatio | ||||||
|       await page.setViewportSize({ width: 1200, height: 500 }) |       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|       await page.addInitScript(async () => { |       await context.addInitScript(async () => { | ||||||
|         localStorage.setItem( |         localStorage.setItem( | ||||||
|           'persistCode', |           'persistCode', | ||||||
|           `exampleSketch = startSketchOn("XZ") |           `exampleSketch = startSketchOn("XZ") | ||||||
|     |> startProfileAt([0, 0], %) |       |> startProfileAt([0, 0], %) | ||||||
|     |> angledLine({ angle: 50, length: 45 }, %) |       |> angledLine({ angle: 50, length: 45 }, %) | ||||||
|     |> yLineTo(0, %) |       |> yLineTo(0, %) | ||||||
|     |> close(%) |       |> close(%) | ||||||
|     |> |       |> | ||||||
|  |    | ||||||
|   example = extrude(5, exampleSketch) |     example = extrude(5, exampleSketch) | ||||||
|   shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)` |     shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)` | ||||||
|         ) |         ) | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|       await expect(async () => { |       await expect(async () => { | ||||||
|         await page.goto('/') |         await homePage.goToModelingScene() | ||||||
|         await u.waitForPageLoad() |         await u.waitForPageLoad() | ||||||
|  |  | ||||||
|         // error in guter |         // error in guter | ||||||
|         await expect(page.locator('.cm-lint-marker-error')).toBeVisible({ |         await expect(page.locator('.cm-lint-marker-error')).toBeVisible({ | ||||||
|           timeout: 1_000, |           timeout: 1_000, | ||||||
| @ -293,12 +308,12 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|  |  | ||||||
|       await expect(page.locator('.cm-content')) |       await expect(page.locator('.cm-content')) | ||||||
|         .toContainText(`exampleSketch = startSketchOn("XZ") |         .toContainText(`exampleSketch = startSketchOn("XZ") | ||||||
|     |> startProfileAt([0, 0], %) |       |> startProfileAt([0, 0], %) | ||||||
|     |> angledLine({ angle: 50, length: 45 }, %) |       |> angledLine({ angle: 50, length: 45 }, %) | ||||||
|     |> yLineTo(0, %) |       |> yLineTo(0, %) | ||||||
|     |> close(%) |       |> close(%) | ||||||
|  |    | ||||||
|     thing: "blah"`) |       thing: "blah"`) | ||||||
|  |  | ||||||
|       await expect(page.locator('.cm-lint-marker-error')).toBeVisible() |       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 ({ |   test('when engine fails export we handle the failure and alert the user', async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|     await page.addInitScript( |     await page.addInitScript( | ||||||
| @ -316,9 +332,10 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|       { code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } |       { 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 |     // wait for execution done | ||||||
|     await u.openDebugPanel() |     await u.openDebugPanel() | ||||||
| @ -374,7 +391,6 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|  |  | ||||||
|     // wait for execution done |     // wait for execution done | ||||||
|     await u.openDebugPanel() |     await u.openDebugPanel() | ||||||
|     await u.clearCommandLogs() |  | ||||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') |     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||||
|     await u.closeDebugPanel() |     await u.closeDebugPanel() | ||||||
|  |  | ||||||
| @ -408,7 +424,7 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|   test( |   test( | ||||||
|     'ensure you can not export while an export is already going', |     'ensure you can not export while an export is already going', | ||||||
|     { tag: ['@skipLinux', '@skipWin'] }, |     { tag: ['@skipLinux', '@skipWin'] }, | ||||||
|     async ({ page }) => { |     async ({ page, homePage }) => { | ||||||
|       // This is being weird on ubuntu and windows. |       // This is being weird on ubuntu and windows. | ||||||
|       test.skip( |       test.skip( | ||||||
|         // eslint-disable-next-line jest/valid-title |         // 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 |         // wait for execution done | ||||||
|         await u.openDebugPanel() |         await u.openDebugPanel() | ||||||
| @ -500,20 +517,17 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|   test( |   test( | ||||||
|     `Network health indicator only appears in modeling view`, |     `Network health indicator only appears in modeling view`, | ||||||
|     { tag: '@electron' }, |     { tag: '@electron' }, | ||||||
|     async ({ browserName: _ }, testInfo) => { |     async ({ context, page }, testInfo) => { | ||||||
|       const { electronApp, page } = await setupElectron({ |       await context.folderSetupFn(async (dir) => { | ||||||
|         testInfo, |         const bracketDir = path.join(dir, 'bracket') | ||||||
|         folderSetupFn: async (dir) => { |         await fsp.mkdir(bracketDir, { recursive: true }) | ||||||
|           const bracketDir = join(dir, 'bracket') |         await fsp.copyFile( | ||||||
|           await fsp.mkdir(bracketDir, { recursive: true }) |           executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||||
|           await fsp.copyFile( |           path.join(bracketDir, 'main.kcl') | ||||||
|             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 u = await getUtils(page) |       const u = await getUtils(page) | ||||||
|  |  | ||||||
|       // Locators |       // Locators | ||||||
| @ -539,18 +553,19 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|         await u.waitForPageLoad() |         await u.waitForPageLoad() | ||||||
|         await expect(networkHealthIndicator).toContainText('Connected') |         await expect(networkHealthIndicator).toContainText('Connected') | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|       await electronApp.close() |  | ||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|   test(`View gizmo stays visible even when zoomed out all the way`, async ({ |   test(`View gizmo stays visible even when zoomed out all the way`, async ({ | ||||||
|     page, |     page, | ||||||
|  |     homePage, | ||||||
|   }) => { |   }) => { | ||||||
|  |     // TODO: fix this test on windows after the electron migration | ||||||
|  |     test.skip(process.platform === 'win32', 'Skip on windows') | ||||||
|     const u = await getUtils(page) |     const u = await getUtils(page) | ||||||
|  |  | ||||||
|     // Constants and locators |     // Constants and locators | ||||||
|     const planeColor: [number, number, number] = [161, 220, 155] |     const planeColor: [number, number, number] = [170, 220, 170] | ||||||
|     const bgColor: [number, number, number] = [27, 27, 27] |     const bgColor: [number, number, number] = [27, 27, 27] | ||||||
|     const middlePixelIsColor = async (color: [number, number, number]) => { |     const middlePixelIsColor = async (color: [number, number, number]) => { | ||||||
|       return u.getGreatestPixDiff({ x: 600, y: 250 }, color) |       return u.getGreatestPixDiff({ x: 600, y: 250 }, color) | ||||||
| @ -561,8 +576,9 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|       await page.addInitScript(async () => { |       await page.addInitScript(async () => { | ||||||
|         localStorage.setItem('persistCode', '') |         localStorage.setItem('persistCode', '') | ||||||
|       }) |       }) | ||||||
|       await page.setViewportSize({ width: 1200, height: 500 }) |       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||||
|       await u.waitForAuthSkipAppStart() |       await homePage.goToModelingScene() | ||||||
|  |       await u.waitForPageLoad() | ||||||
|       await u.closeKclCodePanel() |       await u.closeKclCodePanel() | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
| @ -572,7 +588,7 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|           timeout: 5000, |           timeout: 5000, | ||||||
|           message: 'Plane color is visible', |           message: 'Plane color is visible', | ||||||
|         }) |         }) | ||||||
|         .toBeLessThan(15) |         .toBeLessThanOrEqual(15) | ||||||
|  |  | ||||||
|       let maxZoomOuts = 10 |       let maxZoomOuts = 10 | ||||||
|       let middlePixelIsBackgroundColor = |       let middlePixelIsBackgroundColor = | ||||||
| @ -590,7 +606,7 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       expect(middlePixelIsBackgroundColor, { |       expect(middlePixelIsBackgroundColor, { | ||||||
|         message: 'We no longer the default planes', |         message: 'We should not see the default planes', | ||||||
|       }).toBeTruthy() |       }).toBeTruthy() | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
| @ -598,6 +614,38 @@ sketch001 = startSketchAt([-0, -0]) | |||||||
|       await expect(gizmo).toBeVisible() |       await expect(gizmo).toBeVisible() | ||||||
|     }) |     }) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  |   test(`Refreshing the app doesn't cause the stream to pause on long-executing files`, async ({ | ||||||
|  |     context, | ||||||
|  |     homePage, | ||||||
|  |     scene, | ||||||
|  |     toolbar, | ||||||
|  |     viewport, | ||||||
|  |   }) => { | ||||||
|  |     await context.folderSetupFn(async (dir) => { | ||||||
|  |       const legoDir = path.join(dir, 'lego') | ||||||
|  |       await fsp.mkdir(legoDir, { recursive: true }) | ||||||
|  |       await fsp.copyFile( | ||||||
|  |         executorInputPath('lego.kcl'), | ||||||
|  |         path.join(legoDir, 'main.kcl') | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     await test.step(`Test setup`, async () => { | ||||||
|  |       await homePage.openProject('lego') | ||||||
|  |       await toolbar.closePane('code') | ||||||
|  |     }) | ||||||
|  |     await test.step(`Waiting for the loading spinner to disappear`, async () => { | ||||||
|  |       await scene.loadingIndicator.waitFor({ state: 'detached' }) | ||||||
|  |     }) | ||||||
|  |     await test.step(`The part should start loading quickly, not waiting until execution is complete`, async () => { | ||||||
|  |       await scene.expectPixelColor( | ||||||
|  |         [143, 143, 143], | ||||||
|  |         { x: (viewport?.width ?? 1200) / 2, y: (viewport?.height ?? 500) / 2 }, | ||||||
|  |         15 | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
| }) | }) | ||||||
|  |  | ||||||
| async function clickExportButton(page: Page) { | async function clickExportButton(page: Page) { | ||||||
|  | |||||||
| @ -47,7 +47,11 @@ test.beforeEach(async ({ page }) => { | |||||||
|  |  | ||||||
| test.setTimeout(60_000) | 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', |   'exports of each format should work', | ||||||
|   { tag: ['@snapshot', '@skipWin', '@skipMacos'] }, |   { tag: ['@snapshot', '@skipWin', '@skipMacos'] }, | ||||||
|   async ({ page, context }) => { |   async ({ page, context }) => { | ||||||
| @ -371,6 +375,7 @@ const extrudeDefaultPlane = async (context: any, page: any, plane: string) => { | |||||||
|   await u.closeKclCodePanel() |   await u.closeKclCodePanel() | ||||||
|   await expect(page).toHaveScreenshot({ |   await expect(page).toHaveScreenshot({ | ||||||
|     maxDiffPixels: 100, |     maxDiffPixels: 100, | ||||||
|  |     mask: [page.getByTestId('model-state-indicator')], | ||||||
|   }) |   }) | ||||||
|   await u.openKclCodePanel() |   await u.openKclCodePanel() | ||||||
| } | } | ||||||
|  | |||||||
| Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 49 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 52 KiB | 
| Before Width: | Height: | Size: 46 KiB | 
| Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 60 KiB | 
| Before Width: | Height: | Size: 55 KiB | 
| Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 41 KiB | 
| Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB | 
| Before Width: | Height: | Size: 39 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB | 
| Before Width: | Height: | Size: 34 KiB | 
| Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 42 KiB | 
| Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 52 KiB | 
| Before Width: | Height: | Size: 51 KiB | 
| Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB | 
| Before Width: | Height: | Size: 65 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB | 
