Merge remote-tracking branch 'origin' into ryanrosello-og/playwright-test-coverage
| @ -1,3 +1,3 @@ | ||||
| [codespell] | ||||
| ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall | ||||
| skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas,.yarn.lock,**/yarn.lock | ||||
| skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock | ||||
|  | ||||
| @ -2,7 +2,9 @@ NODE_ENV=development | ||||
| DEV=true | ||||
| VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands | ||||
| VITE_KC_API_BASE_URL=https://api.dev.zoo.dev | ||||
| BASE_URL=https://api.dev.zoo.dev | ||||
| VITE_KC_SITE_BASE_URL=https://dev.zoo.dev | ||||
| VITE_KC_SKIP_AUTH=false | ||||
| VITE_KC_CONNECTION_TIMEOUT_MS=5000 | ||||
| VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local" | ||||
| # ONLY add your token in .env.development.local if you want to skip auth, otherwise this token takes precedence! | ||||
| #VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local" | ||||
|  | ||||
| @ -13,6 +13,8 @@ | ||||
|       "plugin:css-modules/recommended" | ||||
|     ], | ||||
|     "rules": { | ||||
|       "@typescript-eslint/no-floating-promises": "error", | ||||
|       "@typescript-eslint/no-misused-promises": "error", | ||||
|       "semi": [ | ||||
|         "error", | ||||
|         "never" | ||||
| @ -24,7 +26,6 @@ | ||||
|       { | ||||
|         "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure | ||||
|         "rules": { | ||||
|           "@typescript-eslint/no-floating-promises": "warn", | ||||
|           "suggest-no-throw/suggest-no-throw": "off", | ||||
|           "testing-library/prefer-screen-queries": "off", | ||||
|           "jest/valid-expect": "off" | ||||
|  | ||||
							
								
								
									
										333
									
								
								.github/workflows/build-test-publish-apps.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -1,4 +1,4 @@ | ||||
| name: build-test-publish-apps | ||||
| name: build-publish-apps | ||||
|  | ||||
| on: | ||||
|   pull_request: | ||||
| @ -21,7 +21,7 @@ concurrency: | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| jobs: | ||||
|   prepare-json-files: | ||||
|   prepare-files: | ||||
|     runs-on: ubuntu-22.04  # seperate job on Ubuntu for easy string manipulations (compared to Windows) | ||||
|     outputs: | ||||
|       version: ${{ steps.export_version.outputs.version }} | ||||
| @ -33,6 +33,19 @@ jobs: | ||||
|           node-version-file: '.nvmrc' | ||||
|           cache: 'yarn' | ||||
|  | ||||
|       - run: yarn install | ||||
|  | ||||
|       - name: Setup Rust | ||||
|         uses: dtolnay/rust-toolchain@stable | ||||
|  | ||||
|       - uses: Swatinem/rust-cache@v2 | ||||
|         with: | ||||
|           workspaces: './src/wasm-lib' | ||||
|  | ||||
|       # TODO: see if we can fetch from main instead if no diff at src/wasm-lib | ||||
|       - name: Run build:wasm | ||||
|         run: "yarn build:wasm" | ||||
|  | ||||
|       - name: Set nightly version | ||||
|         if: github.event_name == 'schedule' | ||||
|         run: | | ||||
| @ -42,36 +55,48 @@ jobs: | ||||
|       # TODO: see if we ned to add updater test URL here https://dl.zoo.dev/releases/modeling-app/updater-test/last_update.json | ||||
|  | ||||
|       - uses: actions/upload-artifact@v3 | ||||
|         if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }} | ||||
|         with: | ||||
|           name: prepared-files | ||||
|           path: | | ||||
|             package.json | ||||
|             src/wasm-lib/pkg/wasm_lib* | ||||
|  | ||||
|       - id: export_version | ||||
|         run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT" | ||||
|  | ||||
|  | ||||
|   build-test-app-macos: | ||||
|     needs: [prepare-json-files] | ||||
|     runs-on: macos-14 | ||||
|   build-apps: | ||||
|     needs: [prepare-files] | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         os: [macos-14, windows-2022, ubuntu-22.04] | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     env: | ||||
|       APPLE_ID: ${{ secrets.APPLE_ID }} | ||||
|       APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | ||||
|       APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | ||||
|       APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | ||||
|       APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | ||||
|       APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | ||||
|       APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||||
|       CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} | ||||
|       CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | ||||
|       CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||||
|       CSC_FOR_PULL_REQUEST: true | ||||
|       VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }} | ||||
|       VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} | ||||
|       WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|  | ||||
|       - uses: actions/download-artifact@v3 | ||||
|         if: github.event_name == 'schedule' | ||||
|         name: prepared-files | ||||
|  | ||||
|       - name: Copy updated .json files | ||||
|         if: github.event_name == 'schedule' | ||||
|       - name: Copy prepared files | ||||
|         run: | | ||||
|           ls -l artifact | ||||
|           cp artifact/package.json package.json | ||||
|           ls -R prepared-files | ||||
|           cp prepared-files/package.json package.json | ||||
|           cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public | ||||
|           mkdir src/wasm-lib/pkg | ||||
|           cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg | ||||
|  | ||||
|       - name: Sync node version and setup cache | ||||
|         uses: actions/setup-node@v4 | ||||
| @ -81,79 +106,10 @@ jobs: | ||||
|  | ||||
|       - run: yarn install | ||||
|  | ||||
|       - name: Setup Rust | ||||
|         uses: dtolnay/rust-toolchain@stable | ||||
|  | ||||
|       - uses: Swatinem/rust-cache@v2 | ||||
|         with: | ||||
|           workspaces: './src/wasm-lib' | ||||
|  | ||||
|       - name: Run build:wasm | ||||
|         run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}" | ||||
|  | ||||
|       # TODO: sign the app (and updater bundle potentially) | ||||
|       - name: Add signing certificate | ||||
|         if: ${{ env.BUILD_RELEASE == 'true' }} | ||||
|         run: chmod +x add-osx-cert.sh && ./add-osx-cert.sh | ||||
|  | ||||
|       - name: Build the app for arm64 | ||||
|         run: "yarn electron-forge make" | ||||
|  | ||||
|       - name: Build the app for x64 | ||||
|         run: "yarn electron-forge make --arch x64" | ||||
|  | ||||
|       - name: List artifacts | ||||
|         run: "ls -R out/make" | ||||
|  | ||||
|       # TODO: add the 'Build for Mac TestFlight (nightly)' stage back | ||||
|  | ||||
|       - uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           path: "out/make/*/*/*/*" | ||||
|  | ||||
|  | ||||
|   build-test-app-windows: | ||||
|     needs: [prepare-json-files] | ||||
|     runs-on: windows-2022 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|  | ||||
|       - uses: actions/download-artifact@v3 | ||||
|  | ||||
|       - name: Copy updated .json files | ||||
|         if: github.event_name == 'schedule' | ||||
|         run: | | ||||
|           ls -l artifact | ||||
|           cp artifact/package.json package.json | ||||
|  | ||||
|       - name: Sync node version and setup cache | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version-file: '.nvmrc' | ||||
|           cache: 'yarn' # Set this to npm, yarn or pnpm. | ||||
|  | ||||
|       - run: yarn install | ||||
|  | ||||
|       - name: Setup Rust | ||||
|         uses: dtolnay/rust-toolchain@stable | ||||
|  | ||||
|       - uses: Swatinem/rust-cache@v2 | ||||
|         with: | ||||
|           workspaces: './src/wasm-lib' | ||||
|  | ||||
|       - name: Run build:wasm manually | ||||
|         shell: bash | ||||
|         env: | ||||
|           MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }} | ||||
|         run: | | ||||
|           mkdir src/wasm-lib/pkg; cd src/wasm-lib | ||||
|           echo "building with ${{ env.MODE }}" | ||||
|           npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }} | ||||
|           cd ../../ | ||||
|           cp src/wasm-lib/pkg/wasm_lib_bg.wasm public | ||||
|       - run: yarn tronb:vite | ||||
|  | ||||
|       - name: Prepare certificate and variables (Windows only) | ||||
|         if: ${{ env.BUILD_RELEASE == 'true' }} | ||||
|         if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }} | ||||
|         run: | | ||||
|           echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12 | ||||
|           cat /d/Certificate_pkcs12.p12 | ||||
| @ -168,7 +124,7 @@ jobs: | ||||
|         shell: bash | ||||
|  | ||||
|       - name: Setup certicate with SSM KSP (Windows only) | ||||
|         if: ${{ env.BUILD_RELEASE == 'true' }} | ||||
|         if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }} | ||||
|         run: | | ||||
|           curl -X GET  https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi | ||||
|           msiexec /i smtools-windows-x64.msi /quiet /qn | ||||
| @ -178,83 +134,22 @@ jobs: | ||||
|           smksp_cert_sync.exe | ||||
|         shell: cmd | ||||
|  | ||||
|       - name: Build the app for x64 | ||||
|         run: "yarn electron-forge make --arch x64" | ||||
|       - name: Build the app | ||||
|         run: yarn electron-builder --config ${{ env.BUILD_RELEASE && '--publish always' || '' }} | ||||
|  | ||||
|       - name: Build the app for arm64 | ||||
|         run: "yarn electron-forge make --arch arm64" | ||||
|  | ||||
|       - name: List artifacts | ||||
|         run: "ls -R out/make" | ||||
|  | ||||
|       - name: Sign using Signtool | ||||
|         if: ${{ env.BUILD_RELEASE == 'true' }} | ||||
|         env: | ||||
|           THUMBPRINT: "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D" | ||||
|           X64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\x64\\Zoo Modeling App-*Setup.exe" | ||||
|           ARM64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\arm64\\Zoo Modeling App-*Setup.exe" | ||||
|         run: | | ||||
|           signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.X64_FILE }}" | ||||
|           signtool.exe verify /v /pa "${{ env.X64_FILE }}" | ||||
|           signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.ARM64_FILE }}" | ||||
|           signtool.exe verify /v /pa "${{ env.ARM64_FILE }}" | ||||
|       - name: List artifacts in out/ | ||||
|         run: ls -R out | ||||
|  | ||||
|       - uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           path: "out/make/*/*/*" | ||||
|  | ||||
|       # TODO: Run e2e tests | ||||
|  | ||||
|  | ||||
|   build-test-app-ubuntu: | ||||
|     needs: [prepare-json-files] | ||||
|     runs-on: ubuntu-22.04 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|  | ||||
|       - uses: actions/download-artifact@v3 | ||||
|         if: github.event_name == 'schedule' | ||||
|  | ||||
|       - name: Copy updated .json files | ||||
|         if: github.event_name == 'schedule' | ||||
|         run: | | ||||
|           ls -l artifact | ||||
|           cp artifact/package.json package.json | ||||
|  | ||||
|       - name: Sync node version and setup cache | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version-file: '.nvmrc' | ||||
|           cache: 'yarn' # Set this to npm, yarn or pnpm. | ||||
|  | ||||
|       - run: yarn install | ||||
|  | ||||
|       - name: Setup Rust | ||||
|         uses: dtolnay/rust-toolchain@stable | ||||
|  | ||||
|       - uses: Swatinem/rust-cache@v2 | ||||
|         with: | ||||
|           workspaces: './src/wasm-lib' | ||||
|  | ||||
|       - name: Run build:wasm | ||||
|         run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}" | ||||
|  | ||||
|       - name: Build the app for arm64 | ||||
|         run: "yarn electron-forge make --arch arm64" | ||||
|  | ||||
|       - name: Build the app for x64 | ||||
|         run: "yarn electron-forge make --arch x64" | ||||
|  | ||||
|       - name: List artifacts | ||||
|         run: "ls -R out/make" | ||||
|           name: out-${{ matrix.os }} | ||||
|           path: | | ||||
|             out/Zoo*.* | ||||
|             out/latest*.yml | ||||
|  | ||||
|       # TODO: add the 'Build for Mac TestFlight (nightly)' stage back | ||||
|  | ||||
|       # TODO: sign the app (and updater bundle potentially) | ||||
|  | ||||
|       - uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           path: "out/make/*/*/*" | ||||
|       # TODO: add the updater tests back | ||||
|  | ||||
|  | ||||
|   publish-apps-release: | ||||
| @ -262,88 +157,76 @@ jobs: | ||||
|     permissions: | ||||
|       contents: write | ||||
|     if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }} | ||||
|     needs: [prepare-json-files, build-test-app-macos, build-test-app-windows, build-test-app-ubuntu] | ||||
|     needs: [prepare-files, build-apps] | ||||
|     env: | ||||
|       VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }} | ||||
|       VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }} | ||||
|       VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} | ||||
|       VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }} | ||||
|       PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }} | ||||
|       NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }} | ||||
|       BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }} | ||||
|       WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }} | ||||
|       NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }} | ||||
|       BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }} | ||||
|       WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }} | ||||
|       URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }} | ||||
|     steps: | ||||
|       - uses: actions/download-artifact@v3 | ||||
|       - uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Generate the update static endpoint | ||||
|         run: | | ||||
|           ls -l artifact/*/*oo* | ||||
|           DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig` | ||||
|           WINDOWS_X86_64_SIG=`cat artifact/msi/*x64*.msi.zip.sig` | ||||
|           WINDOWS_AARCH64_SIG=`cat artifact/msi/*arm64*.msi.zip.sig` | ||||
|           RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION} | ||||
|           jq --null-input \ | ||||
|             --arg version "${VERSION}" \ | ||||
|             --arg pub_date "${PUB_DATE}" \ | ||||
|             --arg notes "${NOTES}" \ | ||||
|             --arg darwin_sig "$DARWIN_SIG" \ | ||||
|             --arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \ | ||||
|             --arg windows_x86_64_sig "$WINDOWS_X86_64_SIG" \ | ||||
|             --arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \ | ||||
|             --arg windows_aarch64_sig "$WINDOWS_AARCH64_SIG" \ | ||||
|             --arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi.zip" \ | ||||
|             '{ | ||||
|               "version": $version, | ||||
|               "pub_date": $pub_date, | ||||
|               "notes": $notes, | ||||
|               "platforms": { | ||||
|                 "darwin-x86_64": { | ||||
|                   "signature": $darwin_sig, | ||||
|                   "url": $darwin_url | ||||
|                 }, | ||||
|                 "darwin-aarch64": { | ||||
|                   "signature": $darwin_sig, | ||||
|                   "url": $darwin_url | ||||
|                 }, | ||||
|                 "windows-x86_64": { | ||||
|                   "signature": $windows_x86_64_sig, | ||||
|                   "url": $windows_x86_64_url | ||||
|                 }, | ||||
|                 "windows-aarch64": { | ||||
|                   "signature": $windows_aarch64_sig, | ||||
|                   "url": $windows_aarch64_url | ||||
|                 } | ||||
|               } | ||||
|             }' > last_update.json | ||||
|             cat last_update.json | ||||
|       - uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           name: out-windows-2022 | ||||
|           path: out | ||||
|  | ||||
|       - uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           name: out-macos-14 | ||||
|           path: out | ||||
|  | ||||
|       - uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           name: out-ubuntu-22.04 | ||||
|           path: out | ||||
|  | ||||
|       - name: Generate the download static endpoint | ||||
|         run: | | ||||
|           RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION} | ||||
|           RELEASE_DIR=https://${WEBSITE_DIR} | ||||
|           jq --null-input \ | ||||
|             --arg version "${VERSION}" \ | ||||
|             --arg pub_date "${PUB_DATE}" \ | ||||
|             --arg notes "${NOTES}" \ | ||||
|             --arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \ | ||||
|             --arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \ | ||||
|             --arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi" \ | ||||
|             --arg mac_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-mac.dmg" \ | ||||
|             --arg mac_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-mac.dmg" \ | ||||
|             --arg windows_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-win.exe" \ | ||||
|             --arg windows_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-win.exe" \ | ||||
|             --arg linux_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-linux.AppImage" \ | ||||
|             --arg linux_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x86_64-linux.AppImage" \ | ||||
|             '{ | ||||
|               "version": $version, | ||||
|               "pub_date": $pub_date, | ||||
|               "notes": $notes, | ||||
|               "platforms": { | ||||
|                 "dmg-universal": { | ||||
|                   "url": $darwin_url | ||||
|                 "dmg-arm64": { | ||||
|                   "url": $mac_arm64_url | ||||
|                 }, | ||||
|                 "msi-x86_64": { | ||||
|                   "url": $windows_x86_64_url | ||||
|                 "dmg-x64": { | ||||
|                   "url": $mac_x64_url | ||||
|                 }, | ||||
|                 "msi-aarch64": { | ||||
|                   "url": $windows_aarch64_url | ||||
|                 "exe-arm64": { | ||||
|                   "url": $windows_arm64_url | ||||
|                 }, | ||||
|                 "exe-x64": { | ||||
|                   "url": $windows_x64_url | ||||
|                 }, | ||||
|                 "appimage-arm64": { | ||||
|                   "url": $linux_arm64_url | ||||
|                 }, | ||||
|                 "appimage-x64": { | ||||
|                   "url": $linux_x64_url | ||||
|                 } | ||||
|               } | ||||
|             }' > last_download.json | ||||
|             cat last_download.json | ||||
|  | ||||
|       - name: List artifacts | ||||
|         run: "ls -R out" | ||||
|  | ||||
|       - name: Authenticate to Google Cloud | ||||
|         uses: 'google-github-actions/auth@v2.1.5' | ||||
|         with: | ||||
| @ -352,24 +235,26 @@ jobs: | ||||
|       - name: Set up Google Cloud SDK | ||||
|         uses: google-github-actions/setup-gcloud@v2.1.0 | ||||
|         with: | ||||
|           project_id: kittycadapi | ||||
|           project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }} | ||||
|  | ||||
|       - name: Upload release files to public bucket | ||||
|         uses: google-github-actions/upload-cloud-storage@v2.1.3 | ||||
|         uses: google-github-actions/upload-cloud-storage@v2.2.0 | ||||
|         with: | ||||
|           path: artifact | ||||
|           glob: '*/Zoo*' | ||||
|           path: out | ||||
|           glob: 'Zoo*' | ||||
|           parent: false | ||||
|           destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }} | ||||
|           destination: ${{ env.BUCKET_DIR }} | ||||
|  | ||||
|       - name: Upload update endpoint to public bucket | ||||
|         uses: google-github-actions/upload-cloud-storage@v2.1.3 | ||||
|         uses: google-github-actions/upload-cloud-storage@v2.2.0 | ||||
|         with: | ||||
|           path: last_update.json | ||||
|           path: out | ||||
|           glob: 'latest*' | ||||
|           parent: false | ||||
|           destination: ${{ env.BUCKET_DIR }} | ||||
|  | ||||
|       - name: Upload download endpoint to public bucket | ||||
|         uses: google-github-actions/upload-cloud-storage@v2.1.3 | ||||
|         uses: google-github-actions/upload-cloud-storage@v2.2.0 | ||||
|         with: | ||||
|           path: last_download.json | ||||
|           destination: ${{ env.BUCKET_DIR }} | ||||
| @ -378,7 +263,9 @@ jobs: | ||||
|         if: ${{ github.event_name == 'release' }} | ||||
|         uses: softprops/action-gh-release@v2 | ||||
|         with: | ||||
|           files: 'artifact/*/Zoo*' | ||||
|           files: 'out/Zoo*' | ||||
|  | ||||
|       # TODO: Add GitHub publisher | ||||
|  | ||||
|   announce_release: | ||||
|     needs: [publish-apps-release] | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/build-test-web.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -45,7 +45,7 @@ jobs: | ||||
|       - run: yarn xstate:typegen | ||||
|       - run: yarn tsc | ||||
|       - name: Lint | ||||
|         run: yarn eslint --max-warnings 0 src e2e | ||||
|         run: yarn eslint --max-warnings 0 src e2e packages/codemirror-lsp-client | ||||
|  | ||||
|  | ||||
|   check-typos: | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/cargo-check.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -37,4 +37,4 @@ jobs: | ||||
|           # We specifically want to test the disable-println feature | ||||
|           # Since it is not enabled by default, we need to specify it | ||||
|           # This is used in kcl-lsp | ||||
|           cargo check --all --features disable-println --features pyo3 | ||||
|           cargo check --all --features disable-println --features pyo3 --features cli | ||||
|  | ||||
							
								
								
									
										3
									
								
								.github/workflows/cargo-clippy.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -28,6 +28,7 @@ jobs: | ||||
|         dir: ['src/wasm-lib'] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: taiki-e/install-action@just | ||||
|       - name: Install latest rust | ||||
|         uses: actions-rs/toolchain@v1 | ||||
|         with: | ||||
| @ -41,7 +42,7 @@ jobs: | ||||
|       - name: Run clippy | ||||
|         run: | | ||||
|           cd "${{ matrix.dir }}" | ||||
|           cargo clippy --all --tests --benches -- -D warnings | ||||
|           just lint | ||||
|       # If this fails, run "cargo check" to update Cargo.lock, | ||||
|       # then add Cargo.lock to the PR. | ||||
|       - name: Check Cargo.lock doesn't need updating | ||||
|  | ||||
							
								
								
									
										5
									
								
								.github/workflows/cargo-test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -38,11 +38,6 @@ jobs: | ||||
|         with: | ||||
|           toolchain: stable | ||||
|           override: true | ||||
|       - name: install dependencies | ||||
|         if: matrix.dir ==  'src-tauri' | ||||
|         run: | | ||||
|           sudo apt-get update | ||||
|           sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf | ||||
|       - name: Install vector | ||||
|         run: | | ||||
|           curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh | ||||
|  | ||||
							
								
								
									
										20
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -139,7 +139,7 @@ jobs: | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       with: | ||||
|         name: playwright-report-ubuntu-snapshot-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         path: playwright-report/ | ||||
|         retention-days: 30 | ||||
|         overwrite: true | ||||
| @ -174,14 +174,14 @@ jobs: | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: steps.git-check.outputs.modified == 'true' | ||||
|       with: | ||||
|         name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         path: playwright-report/ | ||||
|         retention-days: 30 | ||||
|     - uses: actions/download-artifact@v4 | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       continue-on-error: true | ||||
|       with: | ||||
|         name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         path: test-results/ | ||||
|     - name: Run playwright/chrome flow (with retries) | ||||
|       id: retry | ||||
| @ -245,14 +245,14 @@ jobs: | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: always() | ||||
|       with: | ||||
|         name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         path: test-results/ | ||||
|         retention-days: 30 | ||||
|         overwrite: true | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: always() | ||||
|       with: | ||||
|         name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} | ||||
|         path: playwright-report/ | ||||
|         retention-days: 30 | ||||
|         overwrite: true | ||||
| @ -270,7 +270,7 @@ jobs: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         os: [ubuntu-latest, windows-latest, macos-14] | ||||
|     timeout-minutes: 30 | ||||
|     timeout-minutes: 40 | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     needs: check-rust-changes | ||||
|     steps: | ||||
| @ -359,7 +359,7 @@ jobs: | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       continue-on-error: true | ||||
|       with: | ||||
|         name: test-results-ubuntu-${{ github.sha }} | ||||
|         name: test-results-${{ matrix.os }}-${{ github.sha }} | ||||
|         path: test-results/ | ||||
|     - name: Run electron tests (with retries) | ||||
|       id: retry | ||||
| @ -389,7 +389,7 @@ jobs: | ||||
|                     echo "retried=true" >>$GITHUB_OUTPUT | ||||
|                     echo "run playwright with last failed tests and retry $retry" | ||||
|                     if [[ "$IS_UBUNTU" == "true" ]]; then | ||||
|                       xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true | ||||
|                       xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --last-failed --grep=@electron || true | ||||
|                     else | ||||
|                       yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true | ||||
|                     fi | ||||
| @ -432,14 +432,14 @@ jobs: | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       with: | ||||
|         name: test-results-electron-${{ github.sha }} | ||||
|         name: test-results-electron-${{ matrix.os }}-${{ github.sha }} | ||||
|         path: test-results/ | ||||
|         retention-days: 30 | ||||
|         overwrite: true | ||||
|     - uses: actions/upload-artifact@v4 | ||||
|       if: ${{ !cancelled() && (success() || failure()) }} | ||||
|       with: | ||||
|         name: playwright-report-electron-${{ github.sha }} | ||||
|         name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }} | ||||
|         path: playwright-report/ | ||||
|         retention-days: 30 | ||||
|         overwrite: true | ||||
|  | ||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -54,19 +54,15 @@ e2e/playwright/export-snapshots/* | ||||
|  | ||||
| ## generated files | ||||
| src/**/*.typegen.ts | ||||
| src-tauri/gen | ||||
|  | ||||
| src/wasm-lib/grackle/stdlib_cube_partial.json | ||||
| Mac_App_Distribution.provisionprofile | ||||
|  | ||||
| *.tsbuildinfo | ||||
| src/wasm-lib/pkg | ||||
|  | ||||
| venv | ||||
| .nyc_output/*.vite/ | ||||
|  | ||||
| # electron | ||||
| out/ | ||||
|  | ||||
| src-tauri/target | ||||
| electron-test-projects-dir | ||||
| electron-test-projects-dir-2 | ||||
|  | ||||
							
								
								
									
										344
									
								
								Info.plist
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,344 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||
| <plist version="1.0"> | ||||
|     <dict> | ||||
|         <key>CFBundleDocumentTypes</key> | ||||
|         <array> | ||||
|             <dict> | ||||
|                 <key>LSItemContentTypes</key> | ||||
|                 <array> | ||||
|                     <string>dev.zoo.kcl</string> | ||||
|                 </array> | ||||
|                 <key>CFBundleTypeName</key> | ||||
|                 <string>KCL</string> | ||||
|                 <key>CFBundleTypeRole</key> | ||||
|                 <string>Editor</string> | ||||
|                 <key>LSTypeIsPackage</key> | ||||
|                 <false/> | ||||
|                 <key>LSHandlerRank</key> | ||||
|                 <string>Owner</string> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>LSItemContentTypes</key> | ||||
|                 <array> | ||||
|                     <string>dev.zoo.toml</string> | ||||
|                 </array> | ||||
|                 <key>CFBundleTypeName</key> | ||||
|                 <string>TOML</string> | ||||
|                 <key>CFBundleTypeRole</key> | ||||
|                 <string>Editor</string> | ||||
|                 <key>LSTypeIsPackage</key> | ||||
|                 <false/> | ||||
|                 <key>LSHandlerRank</key> | ||||
|                 <string>Default</string> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>LSItemContentTypes</key> | ||||
|                 <array> | ||||
|                     <string>dev.zoo.gltf</string> | ||||
|                 </array> | ||||
|                 <key>CFBundleTypeName</key> | ||||
|                 <string>glTF</string> | ||||
|                 <key>CFBundleTypeRole</key> | ||||
|                 <string>Editor</string> | ||||
|                 <key>LSTypeIsPackage</key> | ||||
|                 <false/> | ||||
|                 <key>LSHandlerRank</key> | ||||
|                 <string>Default</string> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>LSItemContentTypes</key> | ||||
|                 <array> | ||||
|                     <string>dev.zoo.glb</string> | ||||
|                 </array> | ||||
|                 <key>CFBundleTypeName</key> | ||||
|                 <string>glb</string> | ||||
|                 <key>CFBundleTypeRole</key> | ||||
|                 <string>Editor</string> | ||||
|                 <key>LSTypeIsPackage</key> | ||||
|                 <false/> | ||||
|                 <key>LSHandlerRank</key> | ||||
|                 <string>Default</string> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>LSItemContentTypes</key> | ||||
|                 <array> | ||||
|                     <string>dev.zoo.step</string> | ||||
|                 </array> | ||||
|                 <key>CFBundleTypeName</key> | ||||
|                 <string>STEP</string> | ||||
|                 <key>CFBundleTypeRole</key> | ||||
|                 <string>Editor</string> | ||||
|                 <key>LSTypeIsPackage</key> | ||||
|                 <false/> | ||||
|                 <key>LSHandlerRank</key> | ||||
|                 <string>Default</string> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>LSItemContentTypes</key> | ||||
|                 <array> | ||||
|                     <string>dev.zoo.fbx</string> | ||||
|                 </array> | ||||
|                 <key>CFBundleTypeName</key> | ||||
|                 <string>FBX</string> | ||||
|                 <key>CFBundleTypeRole</key> | ||||
|                 <string>Editor</string> | ||||
|                 <key>LSTypeIsPackage</key> | ||||
|                 <false/> | ||||
|                 <key>LSHandlerRank</key> | ||||
|                 <string>Default</string> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>LSItemContentTypes</key> | ||||
|                 <array> | ||||
|                     <string>dev.zoo.sldprt</string> | ||||
|                 </array> | ||||
|                 <key>CFBundleTypeName</key> | ||||
|                 <string>Solidworks Part</string> | ||||
|                 <key>CFBundleTypeRole</key> | ||||
|                 <string>Viewer</string> | ||||
|                 <key>LSTypeIsPackage</key> | ||||
|                 <false/> | ||||
|                 <key>LSHandlerRank</key> | ||||
|                 <string>Default</string> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>LSItemContentTypes</key> | ||||
|                 <array> | ||||
|                     <string>public.geometry-definition-format</string> | ||||
|                 </array> | ||||
|                 <key>CFBundleTypeName</key> | ||||
|                 <string>OBJ</string> | ||||
|                 <key>CFBundleTypeRole</key> | ||||
|                 <string>Editor</string> | ||||
|                 <key>LSTypeIsPackage</key> | ||||
|                 <false/> | ||||
|                 <key>LSHandlerRank</key> | ||||
|                 <string>Default</string> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>LSItemContentTypes</key> | ||||
|                 <array> | ||||
|                     <string>public.polygon-file-format</string> | ||||
|                 </array> | ||||
|                 <key>CFBundleTypeName</key> | ||||
|                 <string>PLY</string> | ||||
|                 <key>CFBundleTypeRole</key> | ||||
|                 <string>Editor</string> | ||||
|                 <key>LSTypeIsPackage</key> | ||||
|                 <false/> | ||||
|                 <key>LSHandlerRank</key> | ||||
|                 <string>Default</string> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>LSItemContentTypes</key> | ||||
|                 <array> | ||||
|                     <string>public.standard-tesselated-geometry-format</string> | ||||
|                 </array> | ||||
|                 <key>CFBundleTypeName</key> | ||||
|                 <string>STL</string> | ||||
|                 <key>CFBundleTypeRole</key> | ||||
|                 <string>Editor</string> | ||||
|                 <key>LSTypeIsPackage</key> | ||||
|                 <false/> | ||||
|                 <key>LSHandlerRank</key> | ||||
|                 <string>Default</string> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>LSItemContentTypes</key> | ||||
|                 <array> | ||||
|                     <string>public.folder</string> | ||||
|                 </array> | ||||
|                 <key>CFBundleTypeName</key> | ||||
|                 <string>Folders</string> | ||||
|                 <key>CFBundleTypeRole</key> | ||||
|                 <string>Viewer</string> | ||||
|                 <key>LSHandlerRank</key> | ||||
|                 <string>Alternate</string> | ||||
|             </dict> | ||||
|         </array> | ||||
|         <key>UTExportedTypeDeclarations</key> | ||||
|         <array> | ||||
|             <dict> | ||||
|                 <key>UTTypeIdentifier</key> | ||||
|                 <string>dev.zoo.kcl</string> | ||||
|                 <key>UTTypeReferenceURL</key> | ||||
|                 <string>https://zoo.dev/docs/kcl</string> | ||||
|                 <key>UTTypeConformsTo</key> | ||||
|                 <array> | ||||
|                     <string>public.source-code</string> | ||||
|                     <string>public.data</string> | ||||
|                     <string>public.text</string> | ||||
|                     <string>public.plain-text</string> | ||||
|                     <string>public.3d-content</string> | ||||
|                     <string>public.script</string> | ||||
|                 </array> | ||||
|                 <key>UTTypeDescription</key> | ||||
|                 <string>KCL (KittyCAD Language) document</string> | ||||
|                 <key>UTTypeTagSpecification</key> | ||||
|                 <dict> | ||||
|                     <key>public.filename-extension</key> | ||||
|                     <array> | ||||
|                         <string>kcl</string> | ||||
|                     </array> | ||||
|                     <key>public.mime-type</key> | ||||
|                     <array> | ||||
|                         <string>text/vnd.zoo.kcl</string> | ||||
|                     </array> | ||||
|                 </dict> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>UTTypeIdentifier</key> | ||||
|                 <string>dev.zoo.gltf</string> | ||||
|                 <key>UTTypeReferenceURL</key> | ||||
|                 <string>https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html</string> | ||||
|                 <key>UTTypeConformsTo</key> | ||||
|                 <array> | ||||
|                     <string>public.data</string> | ||||
|                     <string>public.text</string> | ||||
|                     <string>public.plain-text</string> | ||||
|                     <string>public.3d-content</string> | ||||
|                     <string>public.json</string> | ||||
|                 </array> | ||||
|                 <key>UTTypeDescription</key> | ||||
|                 <string>Graphics Library Transmission Format (glTF)</string> | ||||
|                 <key>UTTypeTagSpecification</key> | ||||
|                 <dict> | ||||
|                     <key>public.filename-extension</key> | ||||
|                     <array> | ||||
|                         <string>gltf</string> | ||||
|                     </array> | ||||
|                     <key>public.mime-type</key> | ||||
|                     <array> | ||||
|                         <string>model/gltf+json</string> | ||||
|                     </array> | ||||
|                 </dict> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>UTTypeIdentifier</key> | ||||
|                 <string>dev.zoo.glb</string> | ||||
|                 <key>UTTypeReferenceURL</key> | ||||
|                 <string>https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html</string> | ||||
|                 <key>UTTypeConformsTo</key> | ||||
|                 <array> | ||||
|                     <string>public.data</string> | ||||
|                     <string>public.3d-content</string> | ||||
|                 </array> | ||||
|                 <key>UTTypeDescription</key> | ||||
|                 <string>Graphics Library Transmission Format (glTF) binary</string> | ||||
|                 <key>UTTypeTagSpecification</key> | ||||
|                 <dict> | ||||
|                     <key>public.filename-extension</key> | ||||
|                     <array> | ||||
|                         <string>glb</string> | ||||
|                     </array> | ||||
|                     <key>public.mime-type</key> | ||||
|                     <array> | ||||
|                         <string>model/gltf-binary</string> | ||||
|                     </array> | ||||
|                 </dict> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>UTTypeIdentifier</key> | ||||
|                 <string>dev.zoo.step</string> | ||||
|                 <key>UTTypeReferenceURL</key> | ||||
|                 <string>https://www.loc.gov/preservation/digital/formats/fdd/fdd000448.shtml</string> | ||||
|                 <key>UTTypeConformsTo</key> | ||||
|                 <array> | ||||
|                     <string>public.data</string> | ||||
|                     <string>public.3d-content</string> | ||||
|                     <string>public.text</string> | ||||
|                     <string>public.plain-text</string> | ||||
|                 </array> | ||||
|                 <key>UTTypeDescription</key> | ||||
|                 <string>STEP-file, ISO 10303-21</string> | ||||
|                 <key>UTTypeTagSpecification</key> | ||||
|                 <dict> | ||||
|                     <key>public.filename-extension</key> | ||||
|                     <array> | ||||
|                         <string>step</string> | ||||
|                         <string>stp</string> | ||||
|                     </array> | ||||
|                     <key>public.mime-type</key> | ||||
|                     <array> | ||||
|                         <string>model/step</string> | ||||
|                     </array> | ||||
|                 </dict> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>UTTypeIdentifier</key> | ||||
|                 <string>dev.zoo.sldprt</string> | ||||
|                 <key>UTTypeReferenceURL</key> | ||||
|                 <string>https://docs.fileformat.com/cad/sldprt/</string> | ||||
|                 <key>UTTypeConformsTo</key> | ||||
|                 <array> | ||||
|                     <string>public.data</string> | ||||
|                     <string>public.3d-content</string> | ||||
|                 </array> | ||||
|                 <key>UTTypeDescription</key> | ||||
|                 <string>Solidworks Part</string> | ||||
|                 <key>UTTypeTagSpecification</key> | ||||
|                 <dict> | ||||
|                     <key>public.filename-extension</key> | ||||
|                     <array> | ||||
|                         <string>sldprt</string> | ||||
|                     </array> | ||||
|                     <key>public.mime-type</key> | ||||
|                     <array> | ||||
|                         <string>model/vnd.solidworks.sldprt</string> | ||||
|                     </array> | ||||
|                 </dict> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>UTTypeIdentifier</key> | ||||
|                 <string>dev.zoo.fbx</string> | ||||
|                 <key>UTTypeReferenceURL</key> | ||||
|                 <string>https://en.wikipedia.org/wiki/FBX</string> | ||||
|                 <key>UTTypeConformsTo</key> | ||||
|                 <array> | ||||
|                     <string>public.data</string> | ||||
|                     <string>public.3d-content</string> | ||||
|                 </array> | ||||
|                 <key>UTTypeDescription</key> | ||||
|                 <string>Autodesk Filmbox (FBX) format</string> | ||||
|                 <key>UTTypeTagSpecification</key> | ||||
|                 <dict> | ||||
|                     <key>public.filename-extension</key> | ||||
|                     <array> | ||||
|                         <string>fbx</string> | ||||
|                         <string>fbxb</string> | ||||
|                     </array> | ||||
|                     <key>public.mime-type</key> | ||||
|                     <array> | ||||
|                         <string>model/vnd.autodesk.fbx</string> | ||||
|                     </array> | ||||
|                 </dict> | ||||
|             </dict> | ||||
|             <dict> | ||||
|                 <key>UTTypeIdentifier</key> | ||||
|                 <string>dev.zoo.toml</string> | ||||
|                 <key>UTTypeReferenceURL</key> | ||||
|                 <string>https://toml.io/en/</string> | ||||
|                 <key>UTTypeConformsTo</key> | ||||
|                 <array> | ||||
|                     <string>public.data</string> | ||||
|                     <string>public.text</string> | ||||
|                     <string>public.plain-text</string> | ||||
|                 </array> | ||||
|                 <key>UTTypeDescription</key> | ||||
|                 <string>Tom's Obvious Minimal Language</string> | ||||
|                 <key>UTTypeTagSpecification</key> | ||||
|                 <dict> | ||||
|                     <key>public.filename-extension</key> | ||||
|                     <array> | ||||
|                         <string>kcl</string> | ||||
|                     </array> | ||||
|                     <key>public.mime-type</key> | ||||
|                     <array> | ||||
|                         <string>text/toml</string> | ||||
|                     </array> | ||||
|                 </dict> | ||||
|             </dict> | ||||
|         </array> | ||||
|     </dict> | ||||
| </plist> | ||||
							
								
								
									
										8
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						| @ -7,6 +7,14 @@ XSTATE_TYPEGENS := $(wildcard src/machines/*.typegen.ts) | ||||
| dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS) | ||||
| 	yarn start | ||||
|  | ||||
| # I'm sorry this is so specific to my setup you may as well ignore this. | ||||
| # This is so you don't have to deal with electron windows popping up constantly. | ||||
| # It should work for you other Linux users. | ||||
| lee-electron-test: | ||||
| 	Xephyr -br -ac -noreset -screen 1200x500 :2 & | ||||
| 	DISPLAY=:2 NODE_ENV=development PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn tron:test -g "when using the file tree" | ||||
| 	killall Xephyr | ||||
|  | ||||
| $(XSTATE_TYPEGENS): $(TS_SRC) | ||||
| 	yarn xstate typegen 'src/**/*.ts?(x)' | ||||
|  | ||||
|  | ||||
							
								
								
									
										50
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @ -110,7 +110,6 @@ Which commands from setup are one off vs need to be run every time? | ||||
| The following will need to be run when checking out a new commit and guarantees the build is not stale: | ||||
| ```bash | ||||
| yarn install | ||||
| yarn wasm-prep | ||||
| yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build | ||||
| yarn start # or yarn build:local && yarn serve for slower but more production-like build | ||||
| ``` | ||||
| @ -189,12 +188,22 @@ For more information on fuzzing you can check out | ||||
|  | ||||
| ### Playwright tests | ||||
|  | ||||
| You will need a `./e2e/playwright/playwright-secrets.env` file: | ||||
|  | ||||
| ```bash | ||||
| $ touch ./e2e/playwright/playwright-secrets.env | ||||
| $ cat ./e2e/playwright/playwright-secrets.env | ||||
| token=<dev.zoo.dev/account/api-tokens> | ||||
| snapshottoken=<your-snapshot-token> | ||||
| ``` | ||||
|  | ||||
| For a portable way to run Playwright you'll need Docker. | ||||
|  | ||||
| #### Generic example | ||||
| After that, open a terminal and run: | ||||
|  | ||||
| ```bash | ||||
| docker run --network host  --rm --init -it playwright/chrome:playwright-1.43.1 | ||||
| docker run --network host  --rm --init -it playwright/chrome:playwright-x.xx.x | ||||
| ``` | ||||
|  | ||||
| and in another terminal, run: | ||||
| @ -203,21 +212,27 @@ and in another terminal, run: | ||||
| PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" <test suite> | ||||
| ``` | ||||
|  | ||||
| An example of a `<test suite>` is: `e2e/playwright/flow-tests.spec.ts` | ||||
|  | ||||
| YOU WILL NEED A PLAYWRIGHT-SECRETS.ENV FILE: | ||||
| #### Specific example | ||||
|  | ||||
| open a terminal and run: | ||||
|  | ||||
| ```bash | ||||
| # ./e2e/playwright/playwright-secrets.env | ||||
| token=<your-token> | ||||
| snapshottoken=<your-snapshot-token> | ||||
| docker run --network host  --rm --init -it playwright/chrome:playwright-1.46.0 | ||||
| ``` | ||||
|  | ||||
| and in another terminal, run: | ||||
|  | ||||
| ```bash | ||||
| PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" e2e/playwright/command-bar-tests.spec.ts | ||||
| ``` | ||||
| then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens | ||||
|  | ||||
| run a specific test change the test from `test('...` to `test.only('...` | ||||
| (note if you commit this, the tests will instantly fail without running any of the tests) | ||||
|  | ||||
|  | ||||
| **Gotcha**: running the docker container with a mismatched image against your `./node_modules/playwright` will cause a failure. Make sure the versions are matched and up to date. | ||||
|  | ||||
| run headed | ||||
|  | ||||
| ``` | ||||
| @ -336,25 +351,6 @@ PS: for the debug panel, the following JSON is useful for snapping the camera | ||||
|  | ||||
| </details> | ||||
|  | ||||
| ### Tauri e2e tests | ||||
|  | ||||
| #### Windows (local only until the CI edge version mismatch is fixed) | ||||
|  | ||||
| ``` | ||||
| yarn install | ||||
| yarn build:wasm-dev | ||||
| cp src/wasm-lib/pkg/wasm_lib_bg.wasm public | ||||
| yarn vite build --mode development | ||||
| yarn tauri build --debug -b | ||||
| $env:KITTYCAD_API_TOKEN="<YOUR_KITTYCAD_API_TOKEN>" | ||||
| $env:VITE_KC_API_BASE_URL="https://api.dev.zoo.dev" | ||||
| $env:E2E_TAURI_ENABLED="true" | ||||
| $env:TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}' | ||||
| $env:E2E_APPLICATION=".\src-tauri\target\debug\Zoo Modeling App.exe" | ||||
| Stop-Process -Name msedgedriver | ||||
| yarn wdio run wdio.conf.ts | ||||
| ``` | ||||
|  | ||||
| ## KCL | ||||
|  | ||||
| For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl). | ||||
|  | ||||
| @ -1,24 +0,0 @@ | ||||
| #!/usr/bin/env sh | ||||
| # From https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof | ||||
|  | ||||
| KEY_CHAIN=build.keychain | ||||
| CERTIFICATE_P12=certificate.p12 | ||||
|  | ||||
| # Recreate the certificate from the secure environment variable | ||||
| echo $APPLE_CERTIFICATE | base64 --decode > $CERTIFICATE_P12 | ||||
|  | ||||
| #create a keychain | ||||
| security create-keychain -p actions $KEY_CHAIN | ||||
|  | ||||
| # Make the keychain the default so identities are found | ||||
| security default-keychain -s $KEY_CHAIN | ||||
|  | ||||
| # Unlock the keychain | ||||
| security unlock-keychain -p actions $KEY_CHAIN | ||||
|  | ||||
| security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign; | ||||
|  | ||||
| security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN | ||||
|  | ||||
| # remove certs | ||||
| rm -fr *.p12 | ||||
| @ -22,8 +22,3 @@ once fixed in engine will just start working here with no language changes. | ||||
|  | ||||
| - **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple | ||||
|     chamfer cases work currently. | ||||
|  | ||||
|     Sketching on the chamfered face does not currently work. | ||||
|  | ||||
| - **Shell**: Shell sometimes does not work when arcs or fillets are involved. | ||||
|     We are tracking the engine side bug on this. | ||||
|  | ||||
							
								
								
									
										858
									
								
								docs/kcl/arrayReduce.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										835
									
								
								docs/kcl/hollow.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -19,6 +19,7 @@ layout: manual | ||||
| * [`angledLineToX`](kcl/angledLineToX) | ||||
| * [`angledLineToY`](kcl/angledLineToY) | ||||
| * [`arc`](kcl/arc) | ||||
| * [`arrayReduce`](kcl/arrayReduce) | ||||
| * [`asin`](kcl/asin) | ||||
| * [`assert`](kcl/assert) | ||||
| * [`assertEqual`](kcl/assertEqual) | ||||
| @ -44,6 +45,7 @@ layout: manual | ||||
| * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) | ||||
| * [`helix`](kcl/helix) | ||||
| * [`hole`](kcl/hole) | ||||
| * [`hollow`](kcl/hollow) | ||||
| * [`import`](kcl/import) | ||||
| * [`inch`](kcl/inch) | ||||
| * [`int`](kcl/int) | ||||
| @ -55,6 +57,7 @@ layout: manual | ||||
| * [`line`](kcl/line) | ||||
| * [`lineTo`](kcl/lineTo) | ||||
| * [`ln`](kcl/ln) | ||||
| * [`loft`](kcl/loft) | ||||
| * [`log`](kcl/log) | ||||
| * [`log10`](kcl/log10) | ||||
| * [`log2`](kcl/log2) | ||||
| @ -62,6 +65,7 @@ layout: manual | ||||
| * [`max`](kcl/max) | ||||
| * [`min`](kcl/min) | ||||
| * [`mm`](kcl/mm) | ||||
| * [`offsetPlane`](kcl/offsetPlane) | ||||
| * [`patternCircular2d`](kcl/patternCircular2d) | ||||
| * [`patternCircular3d`](kcl/patternCircular3d) | ||||
| * [`patternLinear2d`](kcl/patternLinear2d) | ||||
| @ -87,6 +91,7 @@ layout: manual | ||||
| * [`tan`](kcl/tan) | ||||
| * [`tangentialArc`](kcl/tangentialArc) | ||||
| * [`tangentialArcTo`](kcl/tangentialArcTo) | ||||
| * [`tangentialArcToRelative`](kcl/tangentialArcToRelative) | ||||
| * [`tau`](kcl/tau) | ||||
| * [`toDegrees`](kcl/toDegrees) | ||||
| * [`toRadians`](kcl/toRadians) | ||||
|  | ||||
							
								
								
									
										516
									
								
								docs/kcl/loft.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										138
									
								
								docs/kcl/offsetPlane.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										23507
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
							
						
						| @ -37,8 +37,7 @@ const example = extrude(10, exampleSketch) | ||||
| 	offset: number, | ||||
| 	// Radius of the arc. Not to be confused with Raiders of the Lost Ark. | ||||
| 	radius: number, | ||||
| } | | ||||
| [number, number] | ||||
| } | ||||
| ``` | ||||
| * `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. (REQUIRED) | ||||
| ```js | ||||
|  | ||||
							
								
								
									
										863
									
								
								docs/kcl/tangentialArcToRelative.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -6,7 +6,39 @@ test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
| 
 | ||||
| test.describe('Electron user sidebar menu tests', () => { | ||||
| test.describe('Electron app header tests', () => { | ||||
|   test( | ||||
|     'Open Command Palette button has correct shortcut', | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browserName }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async () => {}, | ||||
|       }) | ||||
| 
 | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
| 
 | ||||
|       // No space before the shortcut since it checks textContent.
 | ||||
|       let text | ||||
|       switch (process.platform) { | ||||
|         case 'darwin': | ||||
|           text = 'Commands⌘K' | ||||
|           break | ||||
|         case 'win32': | ||||
|           text = 'CommandsCtrl+K' | ||||
|           break | ||||
|         default: // 'linux' etc.
 | ||||
|           text = 'CommandsCtrl+K' | ||||
|           break | ||||
|       } | ||||
|       const commandsButton = page.getByRole('button', { name: 'Commands' }) | ||||
|       await expect(commandsButton).toBeVisible() | ||||
|       await expect(commandsButton).toHaveText(text) | ||||
| 
 | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
| 
 | ||||
|   test( | ||||
|     'User settings has correct shortcut', | ||||
|     { tag: '@electron' }, | ||||
| @ -96,33 +96,49 @@ async function doBasicSketch(page: Page, openPanes: string[]) { | ||||
|   } | ||||
|  | ||||
|   // deselect line tool | ||||
|   await page.getByTestId('line').click() | ||||
|   const btnLine = page.getByTestId('line') | ||||
|   const btnLineAriaPressed = await btnLine.getAttribute('aria-pressed') | ||||
|   if (btnLineAriaPressed === 'true') { | ||||
|     await btnLine.click() | ||||
|   } | ||||
|  | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0) | ||||
|   if (openPanes.includes('code')) { | ||||
|     await expect | ||||
|       .poll(async () => u.getGreatestPixDiff(line1, TEST_COLORS.WHITE)) | ||||
|       .toBeLessThan(3) | ||||
|     await page.waitForTimeout(100) | ||||
|     await expect | ||||
|       .poll(() => u.getGreatestPixDiff(line1, [249, 249, 249])) | ||||
|       .poll(async () => u.getGreatestPixDiff(line1, [249, 249, 249])) | ||||
|       .toBeLessThan(3) | ||||
|     await page.waitForTimeout(100) | ||||
|   } | ||||
|  | ||||
|   // click between first two clicks to get center of the line | ||||
|   await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10) | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   if (openPanes.includes('code')) { | ||||
|     expect(await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)).toBeLessThan(3) | ||||
|     await expect( | ||||
|       await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE) | ||||
|     ).toBeLessThan(3) | ||||
|     await expect(await u.getGreatestPixDiff(line1, [0, 0, 255])).toBeLessThan(3) | ||||
|   } | ||||
|  | ||||
|   // hold down shift | ||||
|   await page.keyboard.down('Shift') | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   // click between the latest two clicks to get center of the line | ||||
|   await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20) | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   // selected two lines therefore there should be two cursors | ||||
|   if (openPanes.includes('code')) { | ||||
|     await expect(page.locator('.cm-cursor')).toHaveCount(2) | ||||
|     await page.waitForTimeout(100) | ||||
|   } | ||||
|  | ||||
|   await page.getByRole('button', { name: 'Length: open menu' }).click() | ||||
|  | ||||
| @ -1,6 +1,13 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
|  | ||||
| import { getUtils, setup, setupElectron, tearDown } from './test-utils' | ||||
| import { | ||||
|   getUtils, | ||||
|   setup, | ||||
|   setupElectron, | ||||
|   tearDown, | ||||
|   executorInputPath, | ||||
| } from './test-utils' | ||||
| import { join } from 'path' | ||||
| import { bracket } from 'lib/exampleKcl' | ||||
| import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates' | ||||
| import fsp from 'fs/promises' | ||||
| @ -20,9 +27,19 @@ test.describe('Code pane and errors', () => { | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|     // Load the app with the working starter code | ||||
|     await page.addInitScript((code) => { | ||||
|       localStorage.setItem('persistCode', code) | ||||
|     }, bracket) | ||||
|     await page.addInitScript(() => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `// Extruded Triangle | ||||
| const sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([10, 0], %) | ||||
|   |> line([-5, 10], %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| const extrude001 = extrude(5, sketch001)` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
| @ -48,6 +65,8 @@ test.describe('Code pane and errors', () => { | ||||
|   test('Opening and closing the code pane will consistently show error diagnostics', async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     await page.goto('http://localhost:3000') | ||||
|  | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|     // Load the app with the working starter code | ||||
| @ -73,7 +92,7 @@ test.describe('Code pane and errors', () => { | ||||
|  | ||||
|     // Delete a character to break the KCL | ||||
|     await u.openKclCodePanel() | ||||
|     await page.getByText('extrude(').click() | ||||
|     await page.getByText('thickness, bracketLeg1Sketch)').click() | ||||
|     await page.keyboard.press('Backspace') | ||||
|  | ||||
|     // Ensure that a badge appears on the button | ||||
| @ -84,7 +103,7 @@ test.describe('Code pane and errors', () => { | ||||
|  | ||||
|     // error text on hover | ||||
|     await page.hover('.cm-lint-marker-error') | ||||
|     await expect(page.getByText('Unexpected token: |').first()).toBeVisible() | ||||
|     await expect(page.locator('.cm-tooltip').first()).toBeVisible() | ||||
|  | ||||
|     // Close the code pane | ||||
|     await codePaneButton.click() | ||||
| @ -107,7 +126,7 @@ test.describe('Code pane and errors', () => { | ||||
|  | ||||
|     // error text on hover | ||||
|     await page.hover('.cm-lint-marker-error') | ||||
|     await expect(page.getByText('Unexpected token: |').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 ({ | ||||
| @ -223,26 +242,24 @@ test( | ||||
|   'Opening multiple panes persists when switching projects', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     // Setup multiple projects. | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         const routerTemplateDir = join(dir, 'router-template-slate') | ||||
|         const bracketDir = join(dir, 'bracket') | ||||
|         await Promise.all([ | ||||
|           fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }), | ||||
|           fsp.mkdir(`${dir}/bracket`, { recursive: true }), | ||||
|           fsp.mkdir(routerTemplateDir, { recursive: true }), | ||||
|           fsp.mkdir(bracketDir, { recursive: true }), | ||||
|         ]) | ||||
|         await Promise.all([ | ||||
|           fsp.copyFile( | ||||
|             'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', | ||||
|             `${dir}/router-template-slate/main.kcl` | ||||
|             executorInputPath('router-template-slate.kcl'), | ||||
|             join(routerTemplateDir, 'main.kcl') | ||||
|           ), | ||||
|           fsp.copyFile( | ||||
|             'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', | ||||
|             `${dir}/bracket/main.kcl` | ||||
|             executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|             join(bracketDir, 'main.kcl') | ||||
|           ), | ||||
|         ]) | ||||
|       }, | ||||
| @ -256,10 +273,7 @@ test( | ||||
|  | ||||
|       await page.getByText('bracket').click() | ||||
|  | ||||
|       await expect(page.getByTestId('loading')).toBeAttached() | ||||
|       await expect(page.getByTestId('loading')).not.toBeAttached({ | ||||
|         timeout: 20_000, | ||||
|       }) | ||||
|       await u.waitForPageLoad() | ||||
|     }) | ||||
|  | ||||
|     // If they're open by default, we're not actually testing anything. | ||||
| @ -287,16 +301,7 @@ test( | ||||
|  | ||||
|       await page.getByText('router-template-slate').click() | ||||
|  | ||||
|       await expect(page.getByTestId('loading')).toBeAttached() | ||||
|       await expect(page.getByTestId('loading')).not.toBeAttached({ | ||||
|         timeout: 20_000, | ||||
|       }) | ||||
|  | ||||
|       await expect( | ||||
|         page.getByRole('button', { name: 'Start Sketch' }) | ||||
|       ).toBeEnabled({ | ||||
|         timeout: 20_000, | ||||
|       }) | ||||
|       await u.waitForPageLoad() | ||||
|     }) | ||||
|  | ||||
|     await test.step('All panes opened before should be visible', async () => { | ||||
|  | ||||
| @ -124,7 +124,7 @@ const extrude001 = extrude(-10, sketch001)` | ||||
|     await expect(cmdSearchBar).not.toBeVisible() | ||||
|  | ||||
|     // Now try the same, but with the keyboard shortcut, check focus | ||||
|     await page.keyboard.press('Meta+K') | ||||
|     await page.keyboard.press('ControlOrMeta+K') | ||||
|     await expect(cmdSearchBar).toBeVisible() | ||||
|     await expect(cmdSearchBar).toBeFocused() | ||||
|  | ||||
| @ -185,7 +185,7 @@ const extrude001 = extrude(-10, sketch001)` | ||||
|     await page.locator('.cm-content').click() | ||||
|  | ||||
|     // Now try the same, but with the keyboard shortcut, check focus | ||||
|     await page.keyboard.press('Meta+K') | ||||
|     await page.keyboard.press('ControlOrMeta+K') | ||||
|  | ||||
|     let cmdSearchBar = page.getByPlaceholder('Search commands') | ||||
|     await expect(cmdSearchBar).toBeVisible() | ||||
| @ -250,7 +250,7 @@ const extrude001 = extrude(-10, sketch001)` | ||||
|     await page.getByRole('button', { name: 'Extrude' }).isEnabled() | ||||
|  | ||||
|     let cmdSearchBar = page.getByPlaceholder('Search commands') | ||||
|     await page.keyboard.press('Meta+K') | ||||
|     await page.keyboard.press('ControlOrMeta+K') | ||||
|     await expect(cmdSearchBar).toBeVisible() | ||||
|  | ||||
|     // Search for extrude command and choose it | ||||
|  | ||||
| @ -332,7 +332,6 @@ test.describe('Copilot ghost text', () => { | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control' | ||||
|  | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
| @ -349,10 +348,10 @@ test.describe('Copilot ghost text', () => { | ||||
|     ) | ||||
|  | ||||
|     // Going elsewhere in the code should hide the ghost text. | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.down('Shift') | ||||
|     await page.keyboard.press('KeyZ') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|     await page.keyboard.up('Shift') | ||||
|     await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() | ||||
|  | ||||
| @ -368,8 +367,6 @@ test.describe('Copilot ghost text', () => { | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control' | ||||
|  | ||||
|     await page.waitForTimeout(800) | ||||
|     await u.codeLocator.click() | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
| @ -382,17 +379,17 @@ test.describe('Copilot ghost text', () => { | ||||
|     await page.waitForTimeout(800) | ||||
|  | ||||
|     // Ctrl+z | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.press('KeyZ') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|  | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
|  | ||||
|     // Ctrl+shift+z | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.down('Shift') | ||||
|     await page.keyboard.press('KeyZ') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|     await page.keyboard.up('Shift') | ||||
|  | ||||
|     await expect(page.locator('.cm-content')).toHaveText(`{thing: "blah"}`) | ||||
| @ -411,14 +408,14 @@ test.describe('Copilot ghost text', () => { | ||||
|     ) | ||||
|  | ||||
|     // Once for the enter. | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.press('KeyZ') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|  | ||||
|     // Once for the text. | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.press('KeyZ') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|  | ||||
|     await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() | ||||
|  | ||||
|  | ||||
| @ -1,5 +1,11 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { getUtils, setupElectron, tearDown } from './test-utils' | ||||
| import { join } from 'path' | ||||
| import { | ||||
|   getUtils, | ||||
|   setupElectron, | ||||
|   tearDown, | ||||
|   executorInputPath, | ||||
| } from './test-utils' | ||||
| import fsp from 'fs/promises' | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
| @ -10,22 +16,19 @@ test( | ||||
|   'export works on the first try', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         await Promise.all([fsp.mkdir(`${dir}/bracket`, { recursive: true })]) | ||||
|         const bracketDir = join(dir, 'bracket') | ||||
|         await Promise.all([fsp.mkdir(bracketDir, { recursive: true })]) | ||||
|         await Promise.all([ | ||||
|           fsp.copyFile( | ||||
|             'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', | ||||
|             `${dir}/bracket/other.kcl` | ||||
|             executorInputPath('router-template-slate.kcl'), | ||||
|             join(bracketDir, 'other.kcl') | ||||
|           ), | ||||
|           fsp.copyFile( | ||||
|             'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', | ||||
|             `${dir}/bracket/main.kcl` | ||||
|             executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|             join(bracketDir, 'main.kcl') | ||||
|           ), | ||||
|         ]) | ||||
|       }, | ||||
| @ -40,12 +43,6 @@ test( | ||||
|       // open the project | ||||
|       await page.getByText(`bracket`).click() | ||||
|  | ||||
|       // wait for the project to load | ||||
|       await expect(page.getByTestId('loading')).toBeAttached() | ||||
|       await expect(page.getByTestId('loading')).not.toBeAttached({ | ||||
|         timeout: 20_000, | ||||
|       }) | ||||
|  | ||||
|       // expect zero errors in guter | ||||
|       await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
|  | ||||
| @ -53,6 +50,12 @@ test( | ||||
|       const exportButton = page.getByTestId('export-pane-button') | ||||
|       await expect(exportButton).toBeVisible() | ||||
|  | ||||
|       // Wait for the model to finish loading | ||||
|       const modelStateIndicator = page.getByTestId( | ||||
|         'model-state-indicator-execution-done' | ||||
|       ) | ||||
|       await expect(modelStateIndicator).toBeVisible({ timeout: 60000 }) | ||||
|  | ||||
|       const gltfOption = page.getByText('glTF') | ||||
|       const submitButton = page.getByText('Confirm Export') | ||||
|       const exportingToastMessage = page.getByText(`Exporting...`) | ||||
| @ -101,7 +104,7 @@ test( | ||||
|             }, | ||||
|             { timeout: 15_000 } | ||||
|           ) | ||||
|           .toBe(477327) | ||||
|           .toBe(477481) | ||||
|  | ||||
|         // clean up output.gltf | ||||
|         await fsp.rm('output.gltf') | ||||
|  | ||||
| @ -16,7 +16,6 @@ test.describe('Editor tests', () => { | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control' | ||||
|  | ||||
|     // check no error to begin with | ||||
|     await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() | ||||
| @ -29,9 +28,9 @@ test.describe('Editor tests', () => { | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|  | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.press('/') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`const sketch001 = startSketchOn('XY') | ||||
| @ -42,9 +41,9 @@ test.describe('Editor tests', () => { | ||||
|     // |> close(%)`) | ||||
|  | ||||
|     // uncomment the code | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.press('/') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`const sketch001 = startSketchOn('XY') | ||||
| @ -85,6 +84,63 @@ test.describe('Editor tests', () => { | ||||
|     |> close(%)`) | ||||
|   }) | ||||
|  | ||||
|   test('if you click the format button it formats your code and executes so lints are still there', 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(`const sketch_001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)`) | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.closeDebugPanel() | ||||
|  | ||||
|     // error in guter | ||||
|     await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() | ||||
|  | ||||
|     // error text on hover | ||||
|     await page.hover('.cm-lint-marker-info') | ||||
|     await expect( | ||||
|       page.getByText('Identifiers must be lowerCamelCase').first() | ||||
|     ).toBeVisible() | ||||
|  | ||||
|     await page.locator('#code-pane button:first-child').click() | ||||
|     await page.locator('button:has-text("Format code")').click() | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.closeDebugPanel() | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`const sketch_001 = startSketchOn('XY') | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     |> close(%)`) | ||||
|  | ||||
|     // error in guter | ||||
|     await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() | ||||
|  | ||||
|     // error text on hover | ||||
|     await page.hover('.cm-lint-marker-info') | ||||
|     await expect( | ||||
|       page.getByText('Identifiers must be lowerCamelCase').first() | ||||
|     ).toBeVisible() | ||||
|   }) | ||||
|  | ||||
|   test('fold gutters work', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
| @ -148,9 +204,7 @@ test.describe('Editor tests', () => { | ||||
|     // Delete all the code. | ||||
|     await page.locator('.cm-content').click() | ||||
|     // Select all | ||||
|     await page.keyboard.press('Control+A') | ||||
|     await page.keyboard.press('Backspace') | ||||
|     await page.keyboard.press('Meta+A') | ||||
|     await page.keyboard.press('ControlOrMeta+A') | ||||
|     await page.keyboard.press('Backspace') | ||||
|  | ||||
|     await expect(page.locator('.cm-content')).toHaveText(``) | ||||
| @ -244,6 +298,67 @@ test.describe('Editor tests', () => { | ||||
|     |> close(%)`) | ||||
|   }) | ||||
|  | ||||
|   test('if you use the format keyboard binding it formats your code and executes so lints are shown', async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `const sketch_001 = startSketchOn('XY') | ||||
|   |> startProfileAt([-10, -10], %) | ||||
|   |> line([20, 0], %) | ||||
|   |> line([0, 20], %) | ||||
|   |> line([-20, 0], %) | ||||
|   |> close(%)` | ||||
|       ) | ||||
|       localStorage.setItem('disableAxis', 'true') | ||||
|     }) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.closeDebugPanel() | ||||
|  | ||||
|     // error in guter | ||||
|     await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() | ||||
|  | ||||
|     // error text on hover | ||||
|     await page.hover('.cm-lint-marker-info') | ||||
|     await expect( | ||||
|       page.getByText('Identifiers must be lowerCamelCase').first() | ||||
|     ).toBeVisible() | ||||
|  | ||||
|     // focus the editor | ||||
|     await u.codeLocator.click() | ||||
|  | ||||
|     // Hit alt+shift+f to format the code | ||||
|     await page.keyboard.press('Alt+Shift+KeyF') | ||||
|  | ||||
|     await u.openDebugPanel() | ||||
|     await u.expectCmdLog('[data-message-type="execution-done"]') | ||||
|     await u.closeDebugPanel() | ||||
|  | ||||
|     await expect(page.locator('.cm-content')) | ||||
|       .toHaveText(`const sketch_001 = startSketchOn('XY') | ||||
|     |> startProfileAt([-10, -10], %) | ||||
|     |> line([20, 0], %) | ||||
|     |> line([0, 20], %) | ||||
|     |> line([-20, 0], %) | ||||
|     |> close(%)`) | ||||
|  | ||||
|     // error in guter | ||||
|     await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible() | ||||
|  | ||||
|     // error text on hover | ||||
|     await page.hover('.cm-lint-marker-info') | ||||
|     await expect( | ||||
|       page.getByText('Identifiers must be lowerCamelCase').first() | ||||
|     ).toBeVisible() | ||||
|   }) | ||||
|  | ||||
|   test('if you write kcl with lint errors you get lints', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
| @ -402,7 +517,7 @@ test.describe('Editor tests', () => { | ||||
|   const width = 0.500 | ||||
|   const height = 0.500 | ||||
|   const dia = 4 | ||||
|    | ||||
|  | ||||
|   fn squareHole = (l, w) => { | ||||
|     const squareHoleSketch = startSketchOn('XY') | ||||
|     |> startProfileAt([-width / 2, -length / 2], %) | ||||
|  | ||||
							
								
								
									
										279
									
								
								e2e/playwright/file-tree.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,279 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import * as fsp from 'fs/promises' | ||||
| import { getUtils, setup, setupElectron, tearDown } from './test-utils' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }) => { | ||||
|   await setup(context, page) | ||||
| }) | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
|  | ||||
| test.describe('when using the file tree to', () => { | ||||
|   const fromFile = 'main.kcl' | ||||
|   const toFile = 'hello.kcl' | ||||
|  | ||||
|   test( | ||||
|     `rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async () => {}, | ||||
|       }) | ||||
|  | ||||
|       const { | ||||
|         panesOpen, | ||||
|         createAndSelectProject, | ||||
|         pasteCodeInEditor, | ||||
|         renameFile, | ||||
|         editorTextMatches, | ||||
|       } = await getUtils(page, test) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       page.on('console', console.log) | ||||
|  | ||||
|       await panesOpen(['files', 'code']) | ||||
|  | ||||
|       await createAndSelectProject('project-000') | ||||
|  | ||||
|       // File the main.kcl with contents | ||||
|       const kclCube = await fsp.readFile( | ||||
|         'src/wasm-lib/tests/executor/inputs/cube.kcl', | ||||
|         'utf-8' | ||||
|       ) | ||||
|       await pasteCodeInEditor(kclCube) | ||||
|  | ||||
|       await renameFile(fromFile, toFile) | ||||
|       await page.reload() | ||||
|  | ||||
|       await test.step('Postcondition: editor has same content as before the rename', async () => { | ||||
|         await editorTextMatches(kclCube) | ||||
|       }) | ||||
|  | ||||
|       await test.step('Postcondition: opening and closing settings works', async () => { | ||||
|         const settingsOpenButton = page.getByRole('link', { | ||||
|           name: 'settings Settings', | ||||
|         }) | ||||
|         const settingsCloseButton = page.getByTestId('settings-close-button') | ||||
|         await settingsOpenButton.click() | ||||
|         await settingsCloseButton.click() | ||||
|       }) | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     `create many new untitled files they increment their names`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async () => {}, | ||||
|       }) | ||||
|  | ||||
|       const { panesOpen, createAndSelectProject, createNewFile } = | ||||
|         await getUtils(page, test) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       page.on('console', console.log) | ||||
|  | ||||
|       await panesOpen(['files']) | ||||
|  | ||||
|       await createAndSelectProject('project-000') | ||||
|  | ||||
|       await createNewFile('') | ||||
|       await createNewFile('') | ||||
|       await createNewFile('') | ||||
|       await createNewFile('') | ||||
|       await createNewFile('') | ||||
|  | ||||
|       await test.step('Postcondition: there are 5 new Untitled-*.kcl files', async () => { | ||||
|         await expect( | ||||
|           page | ||||
|             .locator('[data-testid="file-pane-scroll-container"] button') | ||||
|             .filter({ hasText: /Untitled[-]?[0-5]?/ }) | ||||
|         ).toHaveCount(5) | ||||
|       }) | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     'create a new file with the same name as an existing file cancels the operation', | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async () => {}, | ||||
|       }) | ||||
|  | ||||
|       const { | ||||
|         openKclCodePanel, | ||||
|         openFilePanel, | ||||
|         createAndSelectProject, | ||||
|         pasteCodeInEditor, | ||||
|         createNewFileAndSelect, | ||||
|         renameFile, | ||||
|         selectFile, | ||||
|         editorTextMatches, | ||||
|       } = await getUtils(page, test) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       page.on('console', console.log) | ||||
|  | ||||
|       await createAndSelectProject('project-000') | ||||
|       await openKclCodePanel() | ||||
|       await openFilePanel() | ||||
|       // File the main.kcl with contents | ||||
|       const kclCube = await fsp.readFile( | ||||
|         'src/wasm-lib/tests/executor/inputs/cube.kcl', | ||||
|         'utf-8' | ||||
|       ) | ||||
|       await pasteCodeInEditor(kclCube) | ||||
|  | ||||
|       const kcl1 = 'main.kcl' | ||||
|       const kcl2 = '2.kcl' | ||||
|  | ||||
|       await createNewFileAndSelect(kcl2) | ||||
|       const kclCylinder = await fsp.readFile( | ||||
|         'src/wasm-lib/tests/executor/inputs/cylinder.kcl', | ||||
|         'utf-8' | ||||
|       ) | ||||
|       await pasteCodeInEditor(kclCylinder) | ||||
|  | ||||
|       await renameFile(kcl2, kcl1) | ||||
|  | ||||
|       await test.step(`Postcondition: ${kcl1} still has the original content`, async () => { | ||||
|         await selectFile(kcl1) | ||||
|         await editorTextMatches(kclCube) | ||||
|       }) | ||||
|  | ||||
|       await test.step(`Postcondition: ${kcl2} still exists with the original content`, async () => { | ||||
|         await selectFile(kcl2) | ||||
|         await editorTextMatches(kclCylinder) | ||||
|       }) | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     'deleting all files recreates a default main.kcl with no code', | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async () => {}, | ||||
|       }) | ||||
|  | ||||
|       const { | ||||
|         panesOpen, | ||||
|         createAndSelectProject, | ||||
|         pasteCodeInEditor, | ||||
|         deleteFile, | ||||
|         editorTextMatches, | ||||
|       } = await getUtils(page, test) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       page.on('console', console.log) | ||||
|  | ||||
|       await panesOpen(['files', 'code']) | ||||
|  | ||||
|       await createAndSelectProject('project-000') | ||||
|       // File the main.kcl with contents | ||||
|       const kclCube = await fsp.readFile( | ||||
|         'src/wasm-lib/tests/executor/inputs/cube.kcl', | ||||
|         'utf-8' | ||||
|       ) | ||||
|       await pasteCodeInEditor(kclCube) | ||||
|  | ||||
|       const kcl1 = 'main.kcl' | ||||
|  | ||||
|       await deleteFile(kcl1) | ||||
|  | ||||
|       await test.step(`Postcondition: ${kcl1} is recreated but has no content`, async () => { | ||||
|         await editorTextMatches('') | ||||
|       }) | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     'loading small file, then large, then back to small', | ||||
|     { | ||||
|       tag: '@electron', | ||||
|     }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       const { page } = await setupElectron({ | ||||
|         testInfo, | ||||
|       }) | ||||
|  | ||||
|       const { | ||||
|         panesOpen, | ||||
|         createAndSelectProject, | ||||
|         pasteCodeInEditor, | ||||
|         createNewFile, | ||||
|         openDebugPanel, | ||||
|         closeDebugPanel, | ||||
|         expectCmdLog, | ||||
|       } = await getUtils(page, test) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       page.on('console', console.log) | ||||
|  | ||||
|       await panesOpen(['files', 'code']) | ||||
|       await createAndSelectProject('project-000') | ||||
|  | ||||
|       // Create a small file | ||||
|       const kclCube = await fsp.readFile( | ||||
|         'src/wasm-lib/tests/executor/inputs/cube.kcl', | ||||
|         'utf-8' | ||||
|       ) | ||||
|       // pasted into main.kcl | ||||
|       await pasteCodeInEditor(kclCube) | ||||
|  | ||||
|       // Create a large lego file | ||||
|       await createNewFile('lego') | ||||
|       const legoFile = page.getByRole('listitem').filter({ | ||||
|         has: page.getByRole('button', { name: 'lego.kcl' }), | ||||
|       }) | ||||
|       await expect(legoFile).toBeVisible({ timeout: 60_000 }) | ||||
|       await legoFile.click() | ||||
|       const kclLego = await fsp.readFile( | ||||
|         'src/wasm-lib/tests/executor/inputs/lego.kcl', | ||||
|         'utf-8' | ||||
|       ) | ||||
|       await pasteCodeInEditor(kclLego) | ||||
|       const mainFile = page.getByRole('listitem').filter({ | ||||
|         has: page.getByRole('button', { name: 'main.kcl' }), | ||||
|       }) | ||||
|  | ||||
|       // Open settings and enable the debug panel | ||||
|       await page | ||||
|         .getByRole('link', { | ||||
|           name: 'settings Settings', | ||||
|         }) | ||||
|         .click() | ||||
|       await page.locator('#showDebugPanel').getByText('OffOn').click() | ||||
|       await page.getByTestId('settings-close-button').click() | ||||
|  | ||||
|       await test.step('swap between small and large files', async () => { | ||||
|         await openDebugPanel() | ||||
|         // Previously created a file so we need to start back at main.kcl | ||||
|         await mainFile.click() | ||||
|         await expectCmdLog('[data-message-type="execution-done"]', 60_000) | ||||
|         // Click the large file | ||||
|         await legoFile.click() | ||||
|         // Once it is building, click back to the smaller file | ||||
|         await mainFile.click() | ||||
|         await expectCmdLog('[data-message-type="execution-done"]', 60_000) | ||||
|         await closeDebugPanel() | ||||
|       }) | ||||
|     } | ||||
|   ) | ||||
| }) | ||||
| @ -1,5 +1,6 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { setupElectron, tearDown } from './test-utils' | ||||
| import { setupElectron, tearDown, executorInputPath } from './test-utils' | ||||
| import { join } from 'path' | ||||
| import fsp from 'fs/promises' | ||||
|  | ||||
| test.afterEach(async ({ page }, testInfo) => { | ||||
| @ -10,17 +11,14 @@ test( | ||||
|   'When machine-api server not found butt is disabled and shows the reason', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         await fsp.mkdir(`${dir}/bracket`, { recursive: true }) | ||||
|         const bracketDir = join(dir, 'bracket') | ||||
|         await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|         await fsp.copyFile( | ||||
|           'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', | ||||
|           `${dir}/bracket/main.kcl` | ||||
|           executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|           join(bracketDir, 'main.kcl') | ||||
|         ) | ||||
|       }, | ||||
|     }) | ||||
| @ -58,17 +56,14 @@ test( | ||||
|   'When machine-api server not found home screen & project status shows the reason', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         await fsp.mkdir(`${dir}/bracket`, { recursive: true }) | ||||
|         const bracketDir = join(dir, 'bracket') | ||||
|         await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|         await fsp.copyFile( | ||||
|           'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', | ||||
|           `${dir}/bracket/main.kcl` | ||||
|           executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|           join(bracketDir, 'main.kcl') | ||||
|         ) | ||||
|       }, | ||||
|     }) | ||||
|  | ||||
| @ -1,6 +1,13 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { join } from 'path' | ||||
| import fsp from 'fs/promises' | ||||
| import { getUtils, setup, setupElectron, tearDown } from './test-utils' | ||||
| import { | ||||
|   getUtils, | ||||
|   setup, | ||||
|   setupElectron, | ||||
|   tearDown, | ||||
|   executorInputPath, | ||||
| } from './test-utils' | ||||
| import { bracket } from 'lib/exampleKcl' | ||||
| import { onboardingPaths } from 'routes/Onboarding/paths' | ||||
| import { | ||||
| @ -347,17 +354,14 @@ test( | ||||
|   'Restarting onboarding on desktop takes one attempt', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browser: _ }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }) | ||||
|         const routerTemplateDir = join(dir, 'router-template-slate') | ||||
|         await fsp.mkdir(routerTemplateDir, { recursive: true }) | ||||
|         await fsp.copyFile( | ||||
|           'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', | ||||
|           `${dir}/router-template-slate/main.kcl` | ||||
|           executorInputPath('router-template-slate.kcl'), | ||||
|           join(routerTemplateDir, 'main.kcl') | ||||
|         ) | ||||
|       }, | ||||
|     }) | ||||
|  | ||||
| @ -1,11 +1,13 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { test, expect, Page } from '@playwright/test' | ||||
| import { | ||||
|   doExport, | ||||
|   executorInputPath, | ||||
|   getUtils, | ||||
|   isOutOfViewInScrollContainer, | ||||
|   Paths, | ||||
|   setupElectron, | ||||
|   tearDown, | ||||
|   createProjectAndRenameIt, | ||||
| } from './test-utils' | ||||
| import fsp from 'fs/promises' | ||||
| import fs from 'fs' | ||||
| @ -45,17 +47,14 @@ test( | ||||
|   'click help/keybindings from project page', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         await fsp.mkdir(`${dir}/bracket`, { recursive: true }) | ||||
|         const bracketDir = join(dir, 'bracket') | ||||
|         await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|         await fsp.copyFile( | ||||
|           'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', | ||||
|           `${dir}/bracket/main.kcl` | ||||
|           executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|           join(bracketDir, 'main.kcl') | ||||
|         ) | ||||
|       }, | ||||
|     }) | ||||
| @ -64,8 +63,6 @@ test( | ||||
|  | ||||
|     page.on('console', console.log) | ||||
|  | ||||
|     page.on('console', console.log) | ||||
|  | ||||
|     // expect to see the text bracket | ||||
|     await expect(page.getByText('bracket')).toBeVisible() | ||||
|  | ||||
| @ -92,17 +89,13 @@ test( | ||||
|   'when code with error first loads you get errors in console', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         await fsp.mkdir(`${dir}/broken-code`, { recursive: true }) | ||||
|         await fsp.mkdir(join(dir, 'broken-code'), { recursive: true }) | ||||
|         await fsp.copyFile( | ||||
|           'src/wasm-lib/tests/executor/inputs/broken-code-test.kcl', | ||||
|           `${dir}/broken-code/main.kcl` | ||||
|           executorInputPath('broken-code-test.kcl'), | ||||
|           join(dir, 'broken-code', 'main.kcl') | ||||
|         ) | ||||
|       }, | ||||
|     }) | ||||
| @ -138,17 +131,14 @@ test.describe('Can export from electron app', () => { | ||||
|       `Can export using ${method}`, | ||||
|       { tag: '@electron' }, | ||||
|       async ({ browserName }, testInfo) => { | ||||
|         test.skip( | ||||
|           process.platform === 'win32', | ||||
|           'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|         ) | ||||
|         const { electronApp, page } = await setupElectron({ | ||||
|           testInfo, | ||||
|           folderSetupFn: async (dir) => { | ||||
|             await fsp.mkdir(`${dir}/bracket`, { recursive: true }) | ||||
|             const bracketDir = join(dir, 'bracket') | ||||
|             await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|             await fsp.copyFile( | ||||
|               'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', | ||||
|               `${dir}/bracket/main.kcl` | ||||
|               executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|               join(bracketDir, 'main.kcl') | ||||
|             ) | ||||
|           }, | ||||
|         }) | ||||
| @ -157,9 +147,6 @@ test.describe('Can export from electron app', () => { | ||||
|         const u = await getUtils(page) | ||||
|  | ||||
|         page.on('console', console.log) | ||||
|         await electronApp.context().addInitScript(async () => { | ||||
|           ;(window as any).playwrightSkipFilePicker = true | ||||
|         }) | ||||
|  | ||||
|         const pointOnModel = { x: 630, y: 280 } | ||||
|  | ||||
| @ -183,10 +170,10 @@ test.describe('Can export from electron app', () => { | ||||
|           // gray at this pixel means the stream has loaded in the most | ||||
|           // user way we can verify it (pixel color) | ||||
|           await expect | ||||
|             .poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), { | ||||
|             .poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), { | ||||
|               timeout: 10_000, | ||||
|             }) | ||||
|             .toBeLessThan(10) | ||||
|             .toBeLessThan(15) | ||||
|         }) | ||||
|  | ||||
|         const exportLocations: Array<Paths> = [] | ||||
| @ -217,7 +204,7 @@ test.describe('Can export from electron app', () => { | ||||
|               }, | ||||
|               { timeout: 15_000 } | ||||
|             ) | ||||
|             .toBe(477327) | ||||
|             .toBe(477481) | ||||
|  | ||||
|           // clean up output.gltf | ||||
|           await fsp.rm('output.gltf') | ||||
| @ -232,10 +219,6 @@ test( | ||||
|   'Rename and delete projects, also spam arrow keys when renaming', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
| @ -468,7 +451,8 @@ test( | ||||
|     await electronApp.close() | ||||
|   } | ||||
| ) | ||||
| test.fixme( | ||||
|  | ||||
| test( | ||||
|   'File in the file pane should open with a single click', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
| @ -508,10 +492,6 @@ test.fixme( | ||||
|  | ||||
|     await file.click() | ||||
|  | ||||
|     await expect(page.getByTestId('loading')).toBeAttached() | ||||
|     await expect(page.getByTestId('loading')).not.toBeAttached({ | ||||
|       timeout: 20_000, | ||||
|     }) | ||||
|     await expect(u.codeLocator).toContainText( | ||||
|       'A mounting bracket for the Focusrite Scarlett Solo audio interface' | ||||
|     ) | ||||
| @ -520,14 +500,73 @@ test.fixme( | ||||
|   } | ||||
| ) | ||||
|  | ||||
| test( | ||||
|   'Nested directories in project without main.kcl do not create main.kcl', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     let testDir: string | undefined | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         await fsp.mkdir(join(dir, 'router-template-slate', 'nested'), { | ||||
|           recursive: true, | ||||
|         }) | ||||
|         await fsp.copyFile( | ||||
|           executorInputPath('router-template-slate.kcl'), | ||||
|           join(dir, 'router-template-slate', 'nested', 'slate.kcl') | ||||
|         ) | ||||
|         await fsp.copyFile( | ||||
|           executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|           join(dir, 'router-template-slate', 'nested', 'bracket.kcl') | ||||
|         ) | ||||
|         testDir = dir | ||||
|       }, | ||||
|     }) | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|  | ||||
|     page.on('console', console.log) | ||||
|  | ||||
|     await test.step('Open the project', async () => { | ||||
|       await page.getByText('router-template-slate').click() | ||||
|       await expect(page.getByTestId('loading')).toBeAttached() | ||||
|       await expect(page.getByTestId('loading')).not.toBeAttached({ | ||||
|         timeout: 20_000, | ||||
|       }) | ||||
|  | ||||
|       // It actually loads. | ||||
|       await expect(u.codeLocator).toContainText('mounting bracket') | ||||
|       await expect(u.codeLocator).toContainText('const radius =') | ||||
|     }) | ||||
|  | ||||
|     await u.openFilePanel() | ||||
|  | ||||
|     // Find the current file. | ||||
|     const filesPane = page.locator('#files-pane') | ||||
|     await expect(filesPane.getByText('bracket.kcl')).toBeVisible() | ||||
|     // But there's no main.kcl in the file tree browser. | ||||
|     await expect(filesPane.getByText('main.kcl')).not.toBeVisible() | ||||
|     // No main.kcl file is created on the filesystem. | ||||
|     expect(testDir).toBeDefined() | ||||
|     if (testDir !== undefined) { | ||||
|       // eslint-disable-next-line jest/no-conditional-expect | ||||
|       await expect( | ||||
|         fsp.access(join(testDir, 'router-template-slate', 'main.kcl')) | ||||
|       ).rejects.toThrow() | ||||
|       // eslint-disable-next-line jest/no-conditional-expect | ||||
|       await expect( | ||||
|         fsp.access(join(testDir, 'router-template-slate', 'nested', 'main.kcl')) | ||||
|       ).rejects.toThrow() | ||||
|     } | ||||
|  | ||||
|     await electronApp.close() | ||||
|   } | ||||
| ) | ||||
|  | ||||
| test( | ||||
|   'Deleting projects, can delete individual project, can still create projects after deleting all', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|     }) | ||||
| @ -535,33 +574,23 @@ test( | ||||
|  | ||||
|     page.on('console', console.log) | ||||
|  | ||||
|     const createProjectAndRenameIt = async (name: string) => | ||||
|       test.step(`Create and rename project ${name}`, async () => { | ||||
|         await page.getByRole('button', { name: 'New project' }).click() | ||||
|         await expect(page.getByText('Successfully created')).toBeVisible() | ||||
|         await expect(page.getByText('Successfully created')).not.toBeVisible() | ||||
|  | ||||
|         await expect(page.getByText(`project-000`)).toBeVisible() | ||||
|         await page.getByText(`project-000`).hover() | ||||
|         await page.getByText(`project-000`).focus() | ||||
|  | ||||
|         await page.getByLabel('sketch').first().click() | ||||
|  | ||||
|         await page.waitForTimeout(100) | ||||
|  | ||||
|         // type "updated project name" | ||||
|         await page.keyboard.press('Backspace') | ||||
|         await page.keyboard.type(name) | ||||
|  | ||||
|         await page.getByLabel('checkmark').last().click() | ||||
|     const createProjectAndRenameItTest = async ({ | ||||
|       name, | ||||
|       page, | ||||
|     }: { | ||||
|       name: string | ||||
|       page: Page | ||||
|     }) => { | ||||
|       await test.step(`Create and rename project ${name}`, async () => { | ||||
|         await createProjectAndRenameIt({ name, page }) | ||||
|       }) | ||||
|     } | ||||
|  | ||||
|     // we need to create the folders so that the order is correct | ||||
|     // creating them ahead of time with fs tools means they all have the same timestamp | ||||
|     await createProjectAndRenameIt('router-template-slate') | ||||
|     // await createProjectAndRenameIt('focusrite_scarlett_mounting_braket') | ||||
|     await createProjectAndRenameIt('bracket') | ||||
|     await createProjectAndRenameIt('lego') | ||||
|     await createProjectAndRenameItTest({ name: 'router-template-slate', page }) | ||||
|     await createProjectAndRenameItTest({ name: 'bracket', page }) | ||||
|     await createProjectAndRenameItTest({ name: 'lego', page }) | ||||
|  | ||||
|     await test.step('delete the middle project, i.e. the bracket project', async () => { | ||||
|       const project = page.getByText('bracket') | ||||
| @ -618,14 +647,52 @@ test( | ||||
|   } | ||||
| ) | ||||
|  | ||||
| test( | ||||
|   'Can load a file with CRLF line endings', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         const routerTemplateDir = join(dir, 'router-template-slate') | ||||
|         await fsp.mkdir(routerTemplateDir, { recursive: true }) | ||||
|  | ||||
|         const file = await fsp.readFile( | ||||
|           executorInputPath('router-template-slate.kcl'), | ||||
|           'utf-8' | ||||
|         ) | ||||
|         // Replace both \r optionally so we don't end up with \r\r\n | ||||
|         const fileWithCRLF = file.replace(/\r?\n/g, '\r\n') | ||||
|         await fsp.writeFile( | ||||
|           join(routerTemplateDir, 'main.kcl'), | ||||
|           fileWithCRLF, | ||||
|           'utf-8' | ||||
|         ) | ||||
|       }, | ||||
|     }) | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|  | ||||
|     page.on('console', console.log) | ||||
|  | ||||
|     await page.getByText('router-template-slate').click() | ||||
|     await expect(page.getByTestId('loading')).toBeAttached() | ||||
|     await expect(page.getByTestId('loading')).not.toBeAttached({ | ||||
|       timeout: 20_000, | ||||
|     }) | ||||
|  | ||||
|     await expect(u.codeLocator).toContainText('routerDiameter') | ||||
|     await expect(u.codeLocator).toContainText('templateGap') | ||||
|     await expect(u.codeLocator).toContainText('minClampingDistance') | ||||
|  | ||||
|     await electronApp.close() | ||||
|   } | ||||
| ) | ||||
|  | ||||
| test( | ||||
|   'Can sort projects on home page', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|     }) | ||||
| @ -635,33 +702,23 @@ test( | ||||
|  | ||||
|     page.on('console', console.log) | ||||
|  | ||||
|     const createProjectAndRenameIt = async (name: string) => | ||||
|       test.step(`Create and rename project ${name}`, async () => { | ||||
|         await page.getByRole('button', { name: 'New project' }).click() | ||||
|         await expect(page.getByText('Successfully created')).toBeVisible() | ||||
|         await expect(page.getByText('Successfully created')).not.toBeVisible() | ||||
|  | ||||
|         await expect(page.getByText(`project-000`)).toBeVisible() | ||||
|         await page.getByText(`project-000`).hover() | ||||
|         await page.getByText(`project-000`).focus() | ||||
|  | ||||
|         await page.getByLabel('sketch').first().click() | ||||
|  | ||||
|         await page.waitForTimeout(100) | ||||
|  | ||||
|         // type "updated project name" | ||||
|         await page.keyboard.press('Backspace') | ||||
|         await page.keyboard.type(name) | ||||
|  | ||||
|         await page.getByLabel('checkmark').last().click() | ||||
|     const createProjectAndRenameItTest = async ({ | ||||
|       name, | ||||
|       page, | ||||
|     }: { | ||||
|       name: string | ||||
|       page: Page | ||||
|     }) => { | ||||
|       await test.step(`Create and rename project ${name}`, async () => { | ||||
|         await createProjectAndRenameIt({ name, page }) | ||||
|       }) | ||||
|     } | ||||
|  | ||||
|     // we need to create the folders so that the order is correct | ||||
|     // creating them ahead of time with fs tools means they all have the same timestamp | ||||
|     await createProjectAndRenameIt('router-template-slate') | ||||
|     // await createProjectAndRenameIt('focusrite_scarlett_mounting_braket') | ||||
|     await createProjectAndRenameIt('bracket') | ||||
|     await createProjectAndRenameIt('lego') | ||||
|     await createProjectAndRenameItTest({ name: 'router-template-slate', page }) | ||||
|     await createProjectAndRenameItTest({ name: 'bracket', page }) | ||||
|     await createProjectAndRenameItTest({ name: 'lego', page }) | ||||
|  | ||||
|     await test.step('should be shorted by modified initially', async () => { | ||||
|       const lastModifiedButton = page.getByRole('button', { | ||||
| @ -748,10 +805,6 @@ test( | ||||
|   'When the project folder is empty, user can create new project and open it.', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ testInfo }) | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
| @ -796,10 +849,10 @@ const extrude001 = extrude(200, sketch001)`) | ||||
|     // gray at this pixel means the stream has loaded in the most | ||||
|     // user way we can verify it (pixel color) | ||||
|     await expect | ||||
|       .poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), { | ||||
|       .poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), { | ||||
|         timeout: 10_000, | ||||
|       }) | ||||
|       .toBeLessThan(10) | ||||
|       .toBeLessThan(15) | ||||
|  | ||||
|     await expect(async () => { | ||||
|       await page.mouse.move(0, 0, { steps: 5 }) | ||||
| @ -807,8 +860,8 @@ const extrude001 = extrude(200, sketch001)`) | ||||
|       await page.mouse.click(pointOnModel.x, pointOnModel.y) | ||||
|       // check user can interact with model by checking it turns yellow | ||||
|       await expect | ||||
|         .poll(() => u.getGreatestPixDiff(pointOnModel, [176, 180, 132])) | ||||
|         .toBeLessThan(10) | ||||
|         .poll(() => u.getGreatestPixDiff(pointOnModel, [180, 180, 137])) | ||||
|         .toBeLessThan(15) | ||||
|     }).toPass({ timeout: 40_000, intervals: [1_000] }) | ||||
|  | ||||
|     await page.getByTestId('app-logo').click() | ||||
| @ -836,25 +889,35 @@ test( | ||||
|   'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         await Promise.all([ | ||||
|           fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }), | ||||
|           fsp.mkdir(`${dir}/bracket`, { recursive: true }), | ||||
|           fsp.mkdir(join(dir, 'router-template-slate'), { recursive: true }), | ||||
|           fsp.mkdir(join(dir, 'bracket'), { recursive: true }), | ||||
|         ]) | ||||
|         await Promise.all([ | ||||
|           fsp.copyFile( | ||||
|             'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', | ||||
|             `${dir}/router-template-slate/main.kcl` | ||||
|             join( | ||||
|               'src', | ||||
|               'wasm-lib', | ||||
|               'tests', | ||||
|               'executor', | ||||
|               'inputs', | ||||
|               'router-template-slate.kcl' | ||||
|             ), | ||||
|             join(dir, 'router-template-slate', 'main.kcl') | ||||
|           ), | ||||
|           fsp.copyFile( | ||||
|             'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', | ||||
|             `${dir}/bracket/main.kcl` | ||||
|             join( | ||||
|               'src', | ||||
|               'wasm-lib', | ||||
|               'tests', | ||||
|               'executor', | ||||
|               'inputs', | ||||
|               'focusrite_scarlett_mounting_braket.kcl' | ||||
|             ), | ||||
|             join(dir, 'bracket', 'main.kcl') | ||||
|           ), | ||||
|         ]) | ||||
|       }, | ||||
| @ -872,24 +935,15 @@ test( | ||||
|  | ||||
|       await page.getByText('bracket').click() | ||||
|  | ||||
|       await expect(page.getByTestId('loading')).toBeAttached() | ||||
|       await expect(page.getByTestId('loading')).not.toBeAttached({ | ||||
|         timeout: 20_000, | ||||
|       }) | ||||
|  | ||||
|       await expect( | ||||
|         page.getByRole('button', { name: 'Start Sketch' }) | ||||
|       ).toBeEnabled({ | ||||
|         timeout: 20_000, | ||||
|       }) | ||||
|       await u.waitForPageLoad() | ||||
|  | ||||
|       // gray at this pixel means the stream has loaded in the most | ||||
|       // user way we can verify it (pixel color) | ||||
|       await expect | ||||
|         .poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), { | ||||
|         .poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), { | ||||
|           timeout: 10_000, | ||||
|         }) | ||||
|         .toBeLessThan(10) | ||||
|         .toBeLessThan(15) | ||||
|     }) | ||||
|  | ||||
|     await test.step('Clicking the logo takes us back to the projects page / home', async () => { | ||||
| @ -906,24 +960,15 @@ test( | ||||
|  | ||||
|       await page.getByText('router-template-slate').click() | ||||
|  | ||||
|       await expect(page.getByTestId('loading')).toBeAttached() | ||||
|       await expect(page.getByTestId('loading')).not.toBeAttached({ | ||||
|         timeout: 20_000, | ||||
|       }) | ||||
|  | ||||
|       await expect( | ||||
|         page.getByRole('button', { name: 'Start Sketch' }) | ||||
|       ).toBeEnabled({ | ||||
|         timeout: 20_000, | ||||
|       }) | ||||
|       await u.waitForPageLoad() | ||||
|  | ||||
|       // gray at this pixel means the stream has loaded in the most | ||||
|       // user way we can verify it (pixel color) | ||||
|       await expect | ||||
|         .poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), { | ||||
|         .poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), { | ||||
|           timeout: 10_000, | ||||
|         }) | ||||
|         .toBeLessThan(10) | ||||
|         .toBeLessThan(15) | ||||
|     }) | ||||
|  | ||||
|     await test.step('Opening the router-template project should load the stream', async () => { | ||||
| @ -1053,10 +1098,6 @@ test( | ||||
|   'Search projects on desktop home', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName: _ }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const projectData = [ | ||||
|       ['basic bracket', 'focusrite_scarlett_mounting_braket.kcl'], | ||||
|       ['basic-cube', 'basic_fillet_cube_end.kcl'], | ||||
| @ -1071,7 +1112,7 @@ test( | ||||
|         for (const [name, file] of projectData) { | ||||
|           await fsp.mkdir(join(dir, name), { recursive: true }) | ||||
|           await fsp.copyFile( | ||||
|             join('src', 'wasm-lib', 'tests', 'executor', 'inputs', file), | ||||
|             executorInputPath(file), | ||||
|             join(dir, name, `main.kcl`) | ||||
|           ) | ||||
|         } | ||||
| @ -1118,14 +1159,11 @@ test( | ||||
|   'file pane is scrollable when there are many files', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         await fsp.mkdir(`${dir}/testProject`, { recursive: true }) | ||||
|         const testDir = join(dir, 'testProject') | ||||
|         await fsp.mkdir(testDir, { recursive: true }) | ||||
|         const fileNames = [ | ||||
|           'angled_line.kcl', | ||||
|           'basic_fillet_cube_close_opposite.kcl', | ||||
| @ -1189,8 +1227,8 @@ test( | ||||
|         ] | ||||
|         for (const fileName of fileNames) { | ||||
|           await fsp.copyFile( | ||||
|             `src/wasm-lib/tests/executor/inputs/${fileName}`, | ||||
|             `${dir}/testProject/${fileName}` | ||||
|             executorInputPath(fileName), | ||||
|             join(testDir, fileName) | ||||
|           ) | ||||
|         } | ||||
|       }, | ||||
| @ -1231,19 +1269,16 @@ test( | ||||
|   'select all in code editor does not actually select all, just what is visible (regression)', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|       folderSetupFn: async (dir) => { | ||||
|         // src/wasm-lib/tests/executor/inputs/mike_stress_test.kcl | ||||
|         const name = 'mike_stress_test' | ||||
|         await fsp.mkdir(`${dir}/${name}`, { recursive: true }) | ||||
|         const testDir = join(dir, name) | ||||
|         await fsp.mkdir(testDir, { recursive: true }) | ||||
|         await fsp.copyFile( | ||||
|           `src/wasm-lib/tests/executor/inputs/${name}.kcl`, | ||||
|           `${dir}/${name}/main.kcl` | ||||
|           executorInputPath(`${name}.kcl`), | ||||
|           join(testDir, 'main.kcl') | ||||
|         ) | ||||
|       }, | ||||
|     }) | ||||
| @ -1254,18 +1289,13 @@ test( | ||||
|  | ||||
|     await page.getByText('mike_stress_test').click() | ||||
|  | ||||
|     const modifier = | ||||
|       process.platform === 'win32' || process.platform === 'linux' | ||||
|         ? 'Control' | ||||
|         : 'Meta' | ||||
|  | ||||
|     await test.step('select all in code editor, check its length', async () => { | ||||
|       await u.codeLocator.click() | ||||
|       // expect u.codeLocator to have some text | ||||
|       await expect(u.codeLocator).toContainText('line(') | ||||
|       await page.keyboard.down(modifier) | ||||
|       await page.keyboard.down('ControlOrMeta') | ||||
|       await page.keyboard.press('KeyA') | ||||
|       await page.keyboard.up(modifier) | ||||
|       await page.keyboard.up('ControlOrMeta') | ||||
|  | ||||
|       // check the length of the selected text | ||||
|       const selectedText = await page.evaluate(() => { | ||||
| @ -1281,9 +1311,9 @@ test( | ||||
|     await test.step('delete all the text, select again and verify there are no characters left', async () => { | ||||
|       await page.keyboard.press('Backspace') | ||||
|  | ||||
|       await page.keyboard.down(modifier) | ||||
|       await page.keyboard.down('ControlOrMeta') | ||||
|       await page.keyboard.press('KeyA') | ||||
|       await page.keyboard.up(modifier) | ||||
|       await page.keyboard.up('ControlOrMeta') | ||||
|  | ||||
|       // check the length of the selected text | ||||
|       const selectedText = await page.evaluate(() => { | ||||
| @ -1302,10 +1332,6 @@ test( | ||||
|   'Settings persist across restarts', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     test.skip( | ||||
|       process.platform === 'win32', | ||||
|       'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|     ) | ||||
|     await test.step('We can change a user setting like theme', async () => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
| @ -1350,27 +1376,16 @@ test.describe('Renaming in the file tree', () => { | ||||
|     'A file you have open', | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       test.skip( | ||||
|         process.platform === 'win32', | ||||
|         'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|       ) | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|       const { electronApp, page, dir } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async (dir) => { | ||||
|           await fsp.mkdir(join(dir, 'Test Project'), { recursive: true }) | ||||
|           const exampleDir = join( | ||||
|             'src', | ||||
|             'wasm-lib', | ||||
|             'tests', | ||||
|             'executor', | ||||
|             'inputs' | ||||
|           ) | ||||
|           await fsp.copyFile( | ||||
|             join(exampleDir, 'basic_fillet_cube_end.kcl'), | ||||
|             executorInputPath('basic_fillet_cube_end.kcl'), | ||||
|             join(dir, 'Test Project', 'main.kcl') | ||||
|           ) | ||||
|           await fsp.copyFile( | ||||
|             join(exampleDir, 'cylinder.kcl'), | ||||
|             executorInputPath('cylinder.kcl'), | ||||
|             join(dir, 'Test Project', 'fileToRename.kcl') | ||||
|           ) | ||||
|         }, | ||||
| @ -1382,6 +1397,16 @@ test.describe('Renaming in the file tree', () => { | ||||
|       // Constants and locators | ||||
|       const projectLink = page.getByText('Test Project') | ||||
|       const projectMenuButton = page.getByTestId('project-sidebar-toggle') | ||||
|       const checkUnRenamedFS = () => { | ||||
|         const filePath = join(dir, 'Test Project', 'fileToRename.kcl') | ||||
|         return fs.existsSync(filePath) | ||||
|       } | ||||
|       const newFileName = 'newFileName' | ||||
|       const checkRenamedFS = () => { | ||||
|         const filePath = join(dir, 'Test Project', `${newFileName}.kcl`) | ||||
|         return fs.existsSync(filePath) | ||||
|       } | ||||
|  | ||||
|       const fileToRename = page | ||||
|         .getByRole('listitem') | ||||
|         .filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) }) | ||||
| @ -1390,7 +1415,6 @@ test.describe('Renaming in the file tree', () => { | ||||
|         .filter({ has: page.getByRole('button', { name: 'newFileName.kcl' }) }) | ||||
|       const renameMenuItem = page.getByRole('button', { name: 'Rename' }) | ||||
|       const renameInput = page.getByPlaceholder('fileToRename.kcl') | ||||
|       const newFileName = 'newFileName' | ||||
|       const codeLocator = page.locator('.cm-content') | ||||
|  | ||||
|       await test.step('Open project and file pane', async () => { | ||||
| @ -1401,6 +1425,8 @@ test.describe('Renaming in the file tree', () => { | ||||
|  | ||||
|         await u.openFilePanel() | ||||
|         await expect(fileToRename).toBeVisible() | ||||
|         expect(checkUnRenamedFS()).toBeTruthy() | ||||
|         expect(checkRenamedFS()).toBeFalsy() | ||||
|         await fileToRename.click() | ||||
|         await expect(projectMenuButton).toContainText('fileToRename.kcl') | ||||
|         await u.openKclCodePanel() | ||||
| @ -1419,6 +1445,8 @@ test.describe('Renaming in the file tree', () => { | ||||
|       await test.step('Verify the file is renamed', async () => { | ||||
|         await expect(fileToRename).not.toBeAttached() | ||||
|         await expect(renamedFile).toBeVisible() | ||||
|         expect(checkUnRenamedFS()).toBeFalsy() | ||||
|         expect(checkRenamedFS()).toBeTruthy() | ||||
|       }) | ||||
|  | ||||
|       await test.step('Verify we navigated', async () => { | ||||
| @ -1442,27 +1470,16 @@ test.describe('Renaming in the file tree', () => { | ||||
|     'A file you do not have open', | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       test.skip( | ||||
|         process.platform === 'win32', | ||||
|         'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|       ) | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|       const { electronApp, page, dir } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async (dir) => { | ||||
|           await fsp.mkdir(join(dir, 'Test Project'), { recursive: true }) | ||||
|           const exampleDir = join( | ||||
|             'src', | ||||
|             'wasm-lib', | ||||
|             'tests', | ||||
|             'executor', | ||||
|             'inputs' | ||||
|           ) | ||||
|           await fsp.copyFile( | ||||
|             join(exampleDir, 'basic_fillet_cube_end.kcl'), | ||||
|             executorInputPath('basic_fillet_cube_end.kcl'), | ||||
|             join(dir, 'Test Project', 'main.kcl') | ||||
|           ) | ||||
|           await fsp.copyFile( | ||||
|             join(exampleDir, 'cylinder.kcl'), | ||||
|             executorInputPath('cylinder.kcl'), | ||||
|             join(dir, 'Test Project', 'fileToRename.kcl') | ||||
|           ) | ||||
|         }, | ||||
| @ -1473,6 +1490,14 @@ test.describe('Renaming in the file tree', () => { | ||||
|  | ||||
|       // Constants and locators | ||||
|       const newFileName = 'newFileName' | ||||
|       const checkUnRenamedFS = () => { | ||||
|         const filePath = join(dir, 'Test Project', 'fileToRename.kcl') | ||||
|         return fs.existsSync(filePath) | ||||
|       } | ||||
|       const checkRenamedFS = () => { | ||||
|         const filePath = join(dir, 'Test Project', `${newFileName}.kcl`) | ||||
|         return fs.existsSync(filePath) | ||||
|       } | ||||
|       const projectLink = page.getByText('Test Project') | ||||
|       const projectMenuButton = page.getByTestId('project-sidebar-toggle') | ||||
|       const fileToRename = page | ||||
| @ -1493,6 +1518,8 @@ test.describe('Renaming in the file tree', () => { | ||||
|  | ||||
|         await u.openFilePanel() | ||||
|         await expect(fileToRename).toBeVisible() | ||||
|         expect(checkUnRenamedFS()).toBeTruthy() | ||||
|         expect(checkRenamedFS()).toBeFalsy() | ||||
|       }) | ||||
|  | ||||
|       await test.step('Rename the file', async () => { | ||||
| @ -1506,6 +1533,8 @@ test.describe('Renaming in the file tree', () => { | ||||
|       await test.step('Verify the file is renamed', async () => { | ||||
|         await expect(fileToRename).not.toBeAttached() | ||||
|         await expect(renamedFile).toBeVisible() | ||||
|         expect(checkUnRenamedFS()).toBeFalsy() | ||||
|         expect(checkRenamedFS()).toBeTruthy() | ||||
|       }) | ||||
|  | ||||
|       await test.step('Verify we have not navigated', async () => { | ||||
| @ -1532,30 +1561,19 @@ test.describe('Renaming in the file tree', () => { | ||||
|     `A folder you're not inside`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       test.skip( | ||||
|         process.platform === 'win32', | ||||
|         'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|       ) | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|       const { electronApp, page, dir } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async (dir) => { | ||||
|           await fsp.mkdir(join(dir, 'Test Project'), { recursive: true }) | ||||
|           await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), { | ||||
|             recursive: true, | ||||
|           }) | ||||
|           const exampleDir = join( | ||||
|             'src', | ||||
|             'wasm-lib', | ||||
|             'tests', | ||||
|             'executor', | ||||
|             'inputs' | ||||
|           ) | ||||
|           await fsp.copyFile( | ||||
|             join(exampleDir, 'basic_fillet_cube_end.kcl'), | ||||
|             executorInputPath('basic_fillet_cube_end.kcl'), | ||||
|             join(dir, 'Test Project', 'main.kcl') | ||||
|           ) | ||||
|           await fsp.copyFile( | ||||
|             join(exampleDir, 'cylinder.kcl'), | ||||
|             executorInputPath('cylinder.kcl'), | ||||
|             join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl') | ||||
|           ) | ||||
|         }, | ||||
| @ -1573,8 +1591,17 @@ test.describe('Renaming in the file tree', () => { | ||||
|       }) | ||||
|       const renamedFolder = page.getByRole('button', { name: 'newFolderName' }) | ||||
|       const renameMenuItem = page.getByRole('button', { name: 'Rename' }) | ||||
|       const renameInput = page.getByPlaceholder('folderToRename') | ||||
|       const originalFolderName = 'folderToRename' | ||||
|       const renameInput = page.getByPlaceholder(originalFolderName) | ||||
|       const newFolderName = 'newFolderName' | ||||
|       const checkUnRenamedFolderFS = () => { | ||||
|         const folderPath = join(dir, 'Test Project', originalFolderName) | ||||
|         return fs.existsSync(folderPath) | ||||
|       } | ||||
|       const checkRenamedFolderFS = () => { | ||||
|         const folderPath = join(dir, 'Test Project', newFolderName) | ||||
|         return fs.existsSync(folderPath) | ||||
|       } | ||||
|  | ||||
|       await test.step('Open project and file pane', async () => { | ||||
|         await expect(projectLink).toBeVisible() | ||||
| @ -1588,6 +1615,8 @@ test.describe('Renaming in the file tree', () => { | ||||
|  | ||||
|         await u.openFilePanel() | ||||
|         await expect(folderToRename).toBeVisible() | ||||
|         expect(checkUnRenamedFolderFS()).toBeTruthy() | ||||
|         expect(checkRenamedFolderFS()).toBeFalsy() | ||||
|       }) | ||||
|  | ||||
|       await test.step('Rename the folder', async () => { | ||||
| @ -1607,6 +1636,8 @@ test.describe('Renaming in the file tree', () => { | ||||
|         await expect(projectMenuButton).toContainText('main.kcl') | ||||
|         await expect(renamedFolder).toBeVisible() | ||||
|         await expect(folderToRename).not.toBeAttached() | ||||
|         expect(checkUnRenamedFolderFS()).toBeFalsy() | ||||
|         expect(checkRenamedFolderFS()).toBeTruthy() | ||||
|       }) | ||||
|  | ||||
|       await electronApp.close() | ||||
| @ -1617,12 +1648,7 @@ test.describe('Renaming in the file tree', () => { | ||||
|     `A folder you are inside`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       test.skip( | ||||
|         process.platform === 'win32', | ||||
|         'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|       ) | ||||
|       const exampleDir = join('src', 'wasm-lib', 'tests', 'executor', 'inputs') | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|       const { electronApp, page, dir } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async (dir) => { | ||||
|           await fsp.mkdir(join(dir, 'Test Project'), { recursive: true }) | ||||
| @ -1630,11 +1656,11 @@ test.describe('Renaming in the file tree', () => { | ||||
|             recursive: true, | ||||
|           }) | ||||
|           await fsp.copyFile( | ||||
|             join(exampleDir, 'basic_fillet_cube_end.kcl'), | ||||
|             executorInputPath('basic_fillet_cube_end.kcl'), | ||||
|             join(dir, 'Test Project', 'main.kcl') | ||||
|           ) | ||||
|           await fsp.copyFile( | ||||
|             join(exampleDir, 'cylinder.kcl'), | ||||
|             executorInputPath('cylinder.kcl'), | ||||
|             join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl') | ||||
|           ) | ||||
|         }, | ||||
| @ -1655,8 +1681,17 @@ test.describe('Renaming in the file tree', () => { | ||||
|         has: page.getByRole('button', { name: 'someFileWithin.kcl' }), | ||||
|       }) | ||||
|       const renameMenuItem = page.getByRole('button', { name: 'Rename' }) | ||||
|       const renameInput = page.getByPlaceholder('folderToRename') | ||||
|       const originalFolderName = 'folderToRename' | ||||
|       const renameInput = page.getByPlaceholder(originalFolderName) | ||||
|       const newFolderName = 'newFolderName' | ||||
|       const checkUnRenamedFolderFS = () => { | ||||
|         const folderPath = join(dir, 'Test Project', originalFolderName) | ||||
|         return fs.existsSync(folderPath) | ||||
|       } | ||||
|       const checkRenamedFolderFS = () => { | ||||
|         const folderPath = join(dir, 'Test Project', newFolderName) | ||||
|         return fs.existsSync(folderPath) | ||||
|       } | ||||
|  | ||||
|       await test.step('Open project and navigate into folder', async () => { | ||||
|         await expect(projectLink).toBeVisible() | ||||
| @ -1679,9 +1714,12 @@ test.describe('Renaming in the file tree', () => { | ||||
|         expect(newUrl).toContain('folderToRename') | ||||
|         expect(newUrl).toContain('someFileWithin.kcl') | ||||
|         expect(newUrl).not.toContain('main.kcl') | ||||
|         expect(checkUnRenamedFolderFS()).toBeTruthy() | ||||
|         expect(checkRenamedFolderFS()).toBeFalsy() | ||||
|       }) | ||||
|  | ||||
|       await test.step('Rename the folder', async () => { | ||||
|         await page.waitForTimeout(2000) | ||||
|         await folderToRename.click({ button: 'right' }) | ||||
|         await expect(renameMenuItem).toBeVisible() | ||||
|         await renameMenuItem.click() | ||||
| @ -1704,9 +1742,124 @@ test.describe('Renaming in the file tree', () => { | ||||
|         expect(url).not.toContain('main.kcl') | ||||
|         expect(url).toContain(newFolderName) | ||||
|         expect(url).toContain('someFileWithin.kcl') | ||||
|         expect(checkUnRenamedFolderFS()).toBeFalsy() | ||||
|         expect(checkRenamedFolderFS()).toBeTruthy() | ||||
|       }) | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
| }) | ||||
|  | ||||
| test.describe('Deleting files from the file pane', () => { | ||||
|   test( | ||||
|     `when main.kcl exists, navigate to main.kcl`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browserName }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async (dir) => { | ||||
|           const testDir = join(dir, 'testProject') | ||||
|           await fsp.mkdir(testDir, { recursive: true }) | ||||
|           await fsp.copyFile( | ||||
|             executorInputPath('cylinder.kcl'), | ||||
|             join(testDir, 'main.kcl') | ||||
|           ) | ||||
|           await fsp.copyFile( | ||||
|             executorInputPath('basic_fillet_cube_end.kcl'), | ||||
|             join(testDir, 'fileToDelete.kcl') | ||||
|           ) | ||||
|         }, | ||||
|       }) | ||||
|       const u = await getUtils(page) | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       page.on('console', console.log) | ||||
|  | ||||
|       // Constants and locators | ||||
|       const projectCard = page.getByText('testProject') | ||||
|       const projectMenuButton = page.getByTestId('project-sidebar-toggle') | ||||
|       const fileToDelete = page | ||||
|         .getByRole('listitem') | ||||
|         .filter({ has: page.getByRole('button', { name: 'fileToDelete.kcl' }) }) | ||||
|       const deleteMenuItem = page.getByRole('button', { name: 'Delete' }) | ||||
|       const deleteConfirmation = page.getByTestId('delete-confirmation') | ||||
|  | ||||
|       await test.step('Open project and navigate to fileToDelete.kcl', async () => { | ||||
|         await projectCard.click() | ||||
|         await u.waitForPageLoad() | ||||
|         await u.openFilePanel() | ||||
|  | ||||
|         await fileToDelete.click() | ||||
|         await u.waitForPageLoad() | ||||
|         await u.openKclCodePanel() | ||||
|         await expect(u.codeLocator).toContainText('getOppositeEdge(thing)') | ||||
|         await u.closeKclCodePanel() | ||||
|       }) | ||||
|  | ||||
|       await test.step('Delete fileToDelete.kcl', async () => { | ||||
|         await fileToDelete.click({ button: 'right' }) | ||||
|         await expect(deleteMenuItem).toBeVisible() | ||||
|         await deleteMenuItem.click() | ||||
|         await expect(deleteConfirmation).toBeVisible() | ||||
|         await deleteConfirmation.click() | ||||
|       }) | ||||
|  | ||||
|       await test.step('Check deletion and navigation', async () => { | ||||
|         await u.waitForPageLoad() | ||||
|         await expect(fileToDelete).not.toBeVisible() | ||||
|         await u.closeFilePanel() | ||||
|         await u.openKclCodePanel() | ||||
|         await expect(u.codeLocator).toContainText('circle(') | ||||
|         await expect(projectMenuButton).toContainText('main.kcl') | ||||
|       }) | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test.fixme('TODO - when main.kcl does not exist', async () => {}) | ||||
| }) | ||||
|  | ||||
| test( | ||||
|   'Original project name persist after onboarding', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     const { electronApp, page } = await setupElectron({ | ||||
|       testInfo, | ||||
|     }) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|  | ||||
|     const getAllProjects = () => page.getByTestId('project-link').all() | ||||
|     page.on('console', console.log) | ||||
|  | ||||
|     await test.step('Should create and name a project called wrist brace', async () => { | ||||
|       await createProjectAndRenameIt({ name: 'wrist brace', page }) | ||||
|     }) | ||||
|  | ||||
|     await test.step('Should go through onboarding', async () => { | ||||
|       await page.getByTestId('user-sidebar-toggle').click() | ||||
|       await page.getByTestId('user-settings').click() | ||||
|       await page.getByRole('button', { name: 'Replay Onboarding' }).click() | ||||
|  | ||||
|       const numberOfOnboardingSteps = 12 | ||||
|       for (let clicks = 0; clicks < numberOfOnboardingSteps; clicks++) { | ||||
|         await page.getByTestId('onboarding-next').click() | ||||
|       } | ||||
|  | ||||
|       await page.getByTestId('project-sidebar-toggle').click() | ||||
|     }) | ||||
|  | ||||
|     await test.step('Should go home after onboarding is completed', async () => { | ||||
|       await page.getByRole('button', { name: 'Go to Home' }).click() | ||||
|     }) | ||||
|  | ||||
|     await test.step('Should show the original project called wrist brace', async () => { | ||||
|       const projectNames = ['Tutorial Project 00', 'wrist brace'] | ||||
|       for (const [index, projectLink] of (await getAllProjects()).entries()) { | ||||
|         await expect(projectLink).toContainText(projectNames[index]) | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|     await electronApp.close() | ||||
|   } | ||||
| ) | ||||
|  | ||||
| @ -1,6 +1,13 @@ | ||||
| import { test, expect, Page } from '@playwright/test' | ||||
| import { join } from 'path' | ||||
| import * as fsp from 'fs/promises' | ||||
| import { getUtils, setup, setupElectron, tearDown } from './test-utils' | ||||
| import { | ||||
|   getUtils, | ||||
|   setup, | ||||
|   setupElectron, | ||||
|   tearDown, | ||||
|   executorInputPath, | ||||
| } from './test-utils' | ||||
| import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates' | ||||
| import { bracket } from 'lib/exampleKcl' | ||||
|  | ||||
| @ -47,6 +54,67 @@ const sketch001 = startSketchAt([-0, -0]) | ||||
|     const crypticErrorText = `ApiError` | ||||
|     await expect(page.getByText(crypticErrorText).first()).toBeVisible() | ||||
|   }) | ||||
|   test('user should not have to press down twice in cmdbar', async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     // because the model has `line([0,0]..` it is valid code, but the model is invalid | ||||
|     // regression test for https://github.com/KittyCAD/modeling-app/issues/3251 | ||||
|     // Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `const sketch2 = startSketchOn("XY") | ||||
| const sketch001 = startSketchAt([-0, -0]) | ||||
|   |> line([0, 0], %) | ||||
|   |> line([-4.84, -5.29], %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%)` | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1000, height: 500 }) | ||||
|  | ||||
|     await page.goto('/') | ||||
|     await u.waitForPageLoad() | ||||
|  | ||||
|     await test.step('Check arrow down works', async () => { | ||||
|       await page.getByTestId('command-bar-open-button').click() | ||||
|  | ||||
|       await page | ||||
|         .getByRole('option', { name: 'floppy disk arrow Export' }) | ||||
|         .click() | ||||
|  | ||||
|       // press arrow down key twice | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|       await page.waitForTimeout(100) | ||||
|       await page.keyboard.press('ArrowDown') | ||||
|  | ||||
|       // STL is the third option, which makes sense for two arrow downs | ||||
|       await expect(page.locator('[data-headlessui-state="active"]')).toHaveText( | ||||
|         'STL' | ||||
|       ) | ||||
|  | ||||
|       await page.keyboard.press('Escape') | ||||
|       await page.waitForTimeout(200) | ||||
|       await page.keyboard.press('Escape') | ||||
|       await page.waitForTimeout(200) | ||||
|     }) | ||||
|  | ||||
|     await test.step('Check arrow up works', async () => { | ||||
|       // theme in test is dark, which is the second option, which means we can test arrow up | ||||
|       await page.getByTestId('command-bar-open-button').click() | ||||
|  | ||||
|       await page.getByText('The overall appearance of the').click() | ||||
|  | ||||
|       await page.keyboard.press('ArrowUp') | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       await expect(page.locator('[data-headlessui-state="active"]')).toHaveText( | ||||
|         'light' | ||||
|       ) | ||||
|     }) | ||||
|   }) | ||||
|   test('executes on load', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(async () => { | ||||
| @ -351,6 +419,7 @@ const sketch001 = startSketchAt([-0, -0]) | ||||
|       await page.addInitScript( | ||||
|         async ({ code }) => { | ||||
|           localStorage.setItem('persistCode', code) | ||||
|           ;(window as any).playwrightSkipFilePicker = true | ||||
|         }, | ||||
|         { | ||||
|           code: bracket, | ||||
| @ -386,20 +455,22 @@ const sketch001 = startSketchAt([-0, -0]) | ||||
|       await test.step('The second export is blocked', async () => { | ||||
|         // Find the toast. | ||||
|         // Look out for the toast message | ||||
|         await expect(exportingToastMessage).toBeVisible() | ||||
|         await expect(alreadyExportingToastMessage).toBeVisible() | ||||
|  | ||||
|         await page.waitForTimeout(1000) | ||||
|         await Promise.all([ | ||||
|           expect(exportingToastMessage.first()).toBeVisible(), | ||||
|           expect(alreadyExportingToastMessage).toBeVisible(), | ||||
|         ]) | ||||
|       }) | ||||
|  | ||||
|       await test.step('The first export still succeeds', async () => { | ||||
|         await expect(exportingToastMessage).not.toBeVisible() | ||||
|         await expect(errorToastMessage).not.toBeVisible() | ||||
|         await expect(engineErrorToastMessage).not.toBeVisible() | ||||
|  | ||||
|         await expect(successToastMessage).toBeVisible() | ||||
|  | ||||
|         await expect(alreadyExportingToastMessage).not.toBeVisible() | ||||
|         await Promise.all([ | ||||
|           expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }), | ||||
|           expect(errorToastMessage).not.toBeVisible(), | ||||
|           expect(engineErrorToastMessage).not.toBeVisible(), | ||||
|           expect(successToastMessage).toBeVisible({ timeout: 15_000 }), | ||||
|           expect(alreadyExportingToastMessage).not.toBeVisible({ | ||||
|             timeout: 15_000, | ||||
|           }), | ||||
|         ]) | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
| @ -412,10 +483,12 @@ const sketch001 = startSketchAt([-0, -0]) | ||||
|       await expect(exportingToastMessage).toBeVisible() | ||||
|  | ||||
|       // Expect it to succeed. | ||||
|       await expect(exportingToastMessage).not.toBeVisible() | ||||
|       await expect(errorToastMessage).not.toBeVisible() | ||||
|       await expect(engineErrorToastMessage).not.toBeVisible() | ||||
|       await expect(alreadyExportingToastMessage).not.toBeVisible() | ||||
|       await Promise.all([ | ||||
|         expect(exportingToastMessage).not.toBeVisible(), | ||||
|         expect(errorToastMessage).not.toBeVisible(), | ||||
|         expect(engineErrorToastMessage).not.toBeVisible(), | ||||
|         expect(alreadyExportingToastMessage).not.toBeVisible(), | ||||
|       ]) | ||||
|  | ||||
|       await expect(successToastMessage).toBeVisible() | ||||
|     }) | ||||
| @ -425,17 +498,14 @@ const sketch001 = startSketchAt([-0, -0]) | ||||
|     `Network health indicator only appears in modeling view`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browserName: _ }, testInfo) => { | ||||
|       test.skip( | ||||
|         process.platform === 'win32', | ||||
|         'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' | ||||
|       ) | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async (dir) => { | ||||
|           await fsp.mkdir(`${dir}/bracket`, { recursive: true }) | ||||
|           const bracketDir = join(dir, 'bracket') | ||||
|           await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|           await fsp.copyFile( | ||||
|             'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', | ||||
|             `${dir}/bracket/main.kcl` | ||||
|             executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|             join(bracketDir, 'main.kcl') | ||||
|           ) | ||||
|         }, | ||||
|       }) | ||||
|  | ||||
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB | 
| Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 46 KiB | 
| Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 43 KiB | 
| Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB | 
| Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB | 
| Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 37 KiB | 
| Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB | 
| Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 35 KiB | 
| Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB | 
| Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 43 KiB | 
| Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB | 
| Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB | 
| Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB | 
| Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 71 KiB | 
| Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB | 
| Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB | 
| Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 42 KiB | 
| Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB | 
| Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 57 KiB | 
| Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB | 
| Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB | 
| Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB | 
| Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB | 
| Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB | 
| Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB | 
| Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB | 
| Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB | 
| Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB | 
| Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB | 
| Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 32 KiB | 
| Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB | 
| @ -2,13 +2,13 @@ import { | ||||
|   expect, | ||||
|   Page, | ||||
|   Download, | ||||
|   TestInfo, | ||||
|   BrowserContext, | ||||
|   TestInfo, | ||||
|   _electron as electron, | ||||
|   Locator, | ||||
|   test, | ||||
| } from '@playwright/test' | ||||
| import { EngineCommand } from 'lang/std/artifactGraph' | ||||
| import os from 'os' | ||||
| import fsp from 'fs/promises' | ||||
| import fsSync from 'fs' | ||||
| import { join } from 'path' | ||||
| @ -28,6 +28,8 @@ import * as TOML from '@iarna/toml' | ||||
| import { SaveSettingsPayload } from 'lib/settings/settingsTypes' | ||||
| import { SETTINGS_FILE_NAME } from 'lib/constants' | ||||
| import { uuidv4 } from 'lib/utils' | ||||
| import { isArray } from 'lib/utils' | ||||
| import { reportRejection } from 'lib/trap' | ||||
|  | ||||
| type TestColor = [number, number, number] | ||||
| export const TEST_COLORS = { | ||||
| @ -46,6 +48,9 @@ export const commonPoints = { | ||||
|   num2: 14.44, | ||||
| } | ||||
|  | ||||
| export const editorSelector = '[role="textbox"][data-language="kcl"]' | ||||
| type PaneId = 'variables' | 'code' | 'files' | 'logs' | ||||
|  | ||||
| async function waitForPageLoadWithRetry(page: Page) { | ||||
|   await expect(async () => { | ||||
|     await page.goto('/') | ||||
| @ -75,11 +80,10 @@ async function waitForPageLoad(page: Page) { | ||||
| } | ||||
|  | ||||
| async function removeCurrentCode(page: Page) { | ||||
|   const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control' | ||||
|   await page.locator('.cm-content').click() | ||||
|   await page.keyboard.down(hotkey) | ||||
|   await page.keyboard.down('ControlOrMeta') | ||||
|   await page.keyboard.press('a') | ||||
|   await page.keyboard.up(hotkey) | ||||
|   await page.keyboard.up('ControlOrMeta') | ||||
|   await page.keyboard.press('Backspace') | ||||
|   await expect(page.locator('.cm-content')).toHaveText('') | ||||
| } | ||||
| @ -207,7 +211,7 @@ export const wiggleMove = async ( | ||||
| } | ||||
|  | ||||
| export const circleMove = async ( | ||||
|   page: any, | ||||
|   page: Page, | ||||
|   x: number, | ||||
|   y: number, | ||||
|   steps: number, | ||||
| @ -313,13 +317,19 @@ export function normaliseKclNumbers(code: string, ignoreZero = true): string { | ||||
|   return replaceNumbers(code) | ||||
| } | ||||
|  | ||||
| export async function getUtils(page: Page) { | ||||
| export async function getUtils(page: Page, test_?: typeof test) { | ||||
|   if (!test) { | ||||
|     console.warn( | ||||
|       'Some methods in getUtils requires test object as second argument' | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   // Chrome devtools protocol session only works in Chromium | ||||
|   const browserType = page.context().browser()?.browserType().name() | ||||
|   const cdpSession = | ||||
|     browserType !== 'chromium' ? null : await page.context().newCDPSession(page) | ||||
|  | ||||
|   return { | ||||
|   const util = { | ||||
|     waitForAuthSkipAppStart: () => waitForAuthAndLsp(page), | ||||
|     waitForPageLoad: () => waitForPageLoad(page), | ||||
|     waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page), | ||||
| @ -432,46 +442,50 @@ export async function getUtils(page: Page) { | ||||
|       } | ||||
|       return maxDiff | ||||
|     }, | ||||
|     doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) => | ||||
|       new Promise(async (resolve) => { | ||||
|         await page.screenshot({ | ||||
|           path: './e2e/playwright/temp1.png', | ||||
|           fullPage: true, | ||||
|         }) | ||||
|         await fn() | ||||
|         const isImageDiff = async () => { | ||||
|     doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) => | ||||
|       new Promise<boolean>((resolve) => { | ||||
|         ;(async () => { | ||||
|           await page.screenshot({ | ||||
|             path: './e2e/playwright/temp2.png', | ||||
|             path: './e2e/playwright/temp1.png', | ||||
|             fullPage: true, | ||||
|           }) | ||||
|           const screenshot1 = PNG.sync.read( | ||||
|             await fsp.readFile('./e2e/playwright/temp1.png') | ||||
|           ) | ||||
|           const screenshot2 = PNG.sync.read( | ||||
|             await fsp.readFile('./e2e/playwright/temp2.png') | ||||
|           ) | ||||
|           const actualDiffCount = pixelMatch( | ||||
|             screenshot1.data, | ||||
|             screenshot2.data, | ||||
|             null, | ||||
|             screenshot1.width, | ||||
|             screenshot2.height | ||||
|           ) | ||||
|           return actualDiffCount > diffCount | ||||
|         } | ||||
|  | ||||
|         // run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times) | ||||
|         let count = 0 | ||||
|         const interval = setInterval(async () => { | ||||
|           count++ | ||||
|           if (await isImageDiff()) { | ||||
|             clearInterval(interval) | ||||
|             resolve(true) | ||||
|           } else if (count > 100) { | ||||
|             clearInterval(interval) | ||||
|             resolve(false) | ||||
|           await fn() | ||||
|           const isImageDiff = async () => { | ||||
|             await page.screenshot({ | ||||
|               path: './e2e/playwright/temp2.png', | ||||
|               fullPage: true, | ||||
|             }) | ||||
|             const screenshot1 = PNG.sync.read( | ||||
|               await fsp.readFile('./e2e/playwright/temp1.png') | ||||
|             ) | ||||
|             const screenshot2 = PNG.sync.read( | ||||
|               await fsp.readFile('./e2e/playwright/temp2.png') | ||||
|             ) | ||||
|             const actualDiffCount = pixelMatch( | ||||
|               screenshot1.data, | ||||
|               screenshot2.data, | ||||
|               null, | ||||
|               screenshot1.width, | ||||
|               screenshot2.height | ||||
|             ) | ||||
|             return actualDiffCount > diffCount | ||||
|           } | ||||
|         }, 50) | ||||
|  | ||||
|           // run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times) | ||||
|           let count = 0 | ||||
|           const interval = setInterval(() => { | ||||
|             ;(async () => { | ||||
|               count++ | ||||
|               if (await isImageDiff()) { | ||||
|                 clearInterval(interval) | ||||
|                 resolve(true) | ||||
|               } else if (count > 100) { | ||||
|                 clearInterval(interval) | ||||
|                 resolve(false) | ||||
|               } | ||||
|             })().catch(reportRejection) | ||||
|           }, 50) | ||||
|         })().catch(reportRejection) | ||||
|       }), | ||||
|     emulateNetworkConditions: async ( | ||||
|       networkOptions: Protocol.Network.emulateNetworkConditionsParameters | ||||
| @ -486,7 +500,127 @@ export async function getUtils(page: Page) { | ||||
|         networkOptions | ||||
|       ) | ||||
|     }, | ||||
|  | ||||
|     toNormalizedCode: (text: string) => { | ||||
|       return text.replace(/\s+/g, '') | ||||
|     }, | ||||
|  | ||||
|     createAndSelectProject: async (hasText: string) => { | ||||
|       return test_?.step( | ||||
|         `Create and select project with text "${hasText}"`, | ||||
|         async () => { | ||||
|           await page.getByTestId('home-new-file').click() | ||||
|           const projectLinksPost = page.getByTestId('project-link') | ||||
|           await projectLinksPost.filter({ hasText }).click() | ||||
|         } | ||||
|       ) | ||||
|     }, | ||||
|  | ||||
|     editorTextMatches: async (code: string) => { | ||||
|       const editor = page.locator(editorSelector) | ||||
|       return expect(editor).toHaveText(code, { useInnerText: true }) | ||||
|     }, | ||||
|  | ||||
|     pasteCodeInEditor: async (code: string) => { | ||||
|       return test?.step('Paste in KCL code', async () => { | ||||
|         const editor = page.locator(editorSelector) | ||||
|         await editor.fill(code) | ||||
|         await util.editorTextMatches(code) | ||||
|       }) | ||||
|     }, | ||||
|  | ||||
|     clickPane: async (paneId: PaneId) => { | ||||
|       return test?.step(`Open ${paneId} pane`, async () => { | ||||
|         await page.getByTestId(paneId + '-pane-button').click() | ||||
|         await expect(page.locator('#' + paneId + '-pane')).toBeVisible() | ||||
|       }) | ||||
|     }, | ||||
|  | ||||
|     createNewFile: async (name: string) => { | ||||
|       return test?.step(`Create a file named ${name}`, async () => { | ||||
|         await page.getByTestId('create-file-button').click() | ||||
|         await page.getByTestId('file-rename-field').fill(name) | ||||
|         await page.keyboard.press('Enter') | ||||
|       }) | ||||
|     }, | ||||
|  | ||||
|     selectFile: async (name: string) => { | ||||
|       return test?.step(`Select ${name}`, async () => { | ||||
|         await page | ||||
|           .locator('[data-testid="file-pane-scroll-container"] button') | ||||
|           .filter({ hasText: name }) | ||||
|           .click() | ||||
|       }) | ||||
|     }, | ||||
|  | ||||
|     createNewFileAndSelect: async (name: string) => { | ||||
|       return test?.step(`Create a file named ${name}, select it`, async () => { | ||||
|         await openFilePanel(page) | ||||
|         await page.getByTestId('create-file-button').click() | ||||
|         await page.getByTestId('file-rename-field').fill(name) | ||||
|         await page.keyboard.press('Enter') | ||||
|         const newFile = page | ||||
|           .locator('[data-testid="file-pane-scroll-container"] button') | ||||
|           .filter({ hasText: name }) | ||||
|  | ||||
|         await expect(newFile).toBeVisible() | ||||
|         await newFile.click() | ||||
|       }) | ||||
|     }, | ||||
|  | ||||
|     renameFile: async (fromName: string, toName: string) => { | ||||
|       return test?.step(`Rename ${fromName} to ${toName}`, async () => { | ||||
|         await page | ||||
|           .locator('[data-testid="file-pane-scroll-container"] button') | ||||
|           .filter({ hasText: fromName }) | ||||
|           .click({ button: 'right' }) | ||||
|         await page.getByTestId('context-menu-rename').click() | ||||
|         await page.getByTestId('file-rename-field').fill(toName) | ||||
|         await page.keyboard.press('Enter') | ||||
|         await page | ||||
|           .locator('[data-testid="file-pane-scroll-container"] button') | ||||
|           .filter({ hasText: toName }) | ||||
|           .click() | ||||
|       }) | ||||
|     }, | ||||
|  | ||||
|     deleteFile: async (name: string) => { | ||||
|       return test?.step(`Delete ${name}`, async () => { | ||||
|         await page | ||||
|           .locator('[data-testid="file-pane-scroll-container"] button') | ||||
|           .filter({ hasText: name }) | ||||
|           .click({ button: 'right' }) | ||||
|         await page.getByTestId('context-menu-delete').click() | ||||
|         await page.getByTestId('delete-confirmation').click() | ||||
|       }) | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * @deprecated Sorry I don't have time to fix this right now, but runs like | ||||
|      * the one linked below show me that setting the open panes in this manner is not reliable. | ||||
|      * You can either set `openPanes` as a part of the same initScript we run in setupElectron/setup, | ||||
|      * or you can imperatively open the panes with functions like {openKclCodePanel} | ||||
|      * (or we can make a general openPane function that takes a paneId)., | ||||
|      * but having a separate initScript does not seem to work reliably. | ||||
|      * @link https://github.com/KittyCAD/modeling-app/actions/runs/10731890169/job/29762700806?pr=3807#step:20:19553 | ||||
|      */ | ||||
|     panesOpen: async (paneIds: PaneId[]) => { | ||||
|       return test?.step(`Setting ${paneIds} panes to be open`, async () => { | ||||
|         await page.addInitScript( | ||||
|           ({ PERSIST_MODELING_CONTEXT, paneIds }) => { | ||||
|             localStorage.setItem( | ||||
|               PERSIST_MODELING_CONTEXT, | ||||
|               JSON.stringify({ openPanes: paneIds }) | ||||
|             ) | ||||
|           }, | ||||
|           { PERSIST_MODELING_CONTEXT, paneIds } | ||||
|         ) | ||||
|         await page.reload() | ||||
|       }) | ||||
|     }, | ||||
|   } | ||||
|  | ||||
|   return util | ||||
| } | ||||
|  | ||||
| type TemplateOptions = Array<number | Array<number>> | ||||
| @ -507,7 +641,7 @@ const _makeTemplate = ( | ||||
|   templateParts: TemplateStringsArray, | ||||
|   ...options: TemplateOptions | ||||
| ) => { | ||||
|   const length = Math.max(...options.map((a) => (Array.isArray(a) ? a[0] : 0))) | ||||
|   const length = Math.max(...options.map((a) => (isArray(a) ? a[0] : 0))) | ||||
|   let reExpTemplate = '' | ||||
|   for (let i = 0; i < length; i++) { | ||||
|     const currentStr = templateParts.map((str, index) => { | ||||
| @ -515,7 +649,7 @@ const _makeTemplate = ( | ||||
|       return ( | ||||
|         escapeRegExp(str) + | ||||
|         String( | ||||
|           Array.isArray(currentOptions) | ||||
|           isArray(currentOptions) | ||||
|             ? currentOptions[i] | ||||
|             : typeof currentOptions === 'number' | ||||
|             ? currentOptions | ||||
| @ -669,11 +803,6 @@ export const doExport = async ( | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the appropriate modifier key for the platform. | ||||
|  */ | ||||
| export const metaModifier = os.platform() === 'darwin' ? 'Meta' : 'Control' | ||||
|  | ||||
| export async function tearDown(page: Page, testInfo: TestInfo) { | ||||
|   if (testInfo.status === 'skipped') return | ||||
|   if (testInfo.status === 'failed') return | ||||
| @ -713,7 +842,6 @@ export async function setup(context: BrowserContext, page: Page) { | ||||
|       localStorage.setItem('persistCode', ``) | ||||
|       localStorage.setItem(settingsKey, settings) | ||||
|       localStorage.setItem(IS_PLAYWRIGHT_KEY, 'true') | ||||
|       console.log('TEST_SETTINGS.projects', settings) | ||||
|     }, | ||||
|     { | ||||
|       token: secrets.token, | ||||
| @ -770,6 +898,7 @@ export async function setup(context: BrowserContext, page: Page) { | ||||
|   // kill animations, speeds up tests and reduced flakiness | ||||
|   await page.emulateMedia({ reducedMotion: 'reduce' }) | ||||
|  | ||||
|   // Trigger a navigation, since loading file:// doesn't. | ||||
|   await page.reload() | ||||
| } | ||||
|  | ||||
| @ -777,10 +906,12 @@ export async function setupElectron({ | ||||
|   testInfo, | ||||
|   folderSetupFn, | ||||
|   cleanProjectDir = true, | ||||
|   appSettings, | ||||
| }: { | ||||
|   testInfo: TestInfo | ||||
|   folderSetupFn?: (projectDirName: string) => Promise<void> | ||||
|   cleanProjectDir?: boolean | ||||
|   appSettings?: Partial<SaveSettingsPayload> | ||||
| }) { | ||||
|   // create or otherwise clear the folder | ||||
|   const projectDirName = testInfo.outputPath('electron-test-projects-dir') | ||||
| @ -814,15 +945,19 @@ export async function setupElectron({ | ||||
|  | ||||
|   if (cleanProjectDir) { | ||||
|     const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME) | ||||
|     const settingsOverrides = TOML.stringify({ | ||||
|       ...TEST_SETTINGS, | ||||
|       settings: { | ||||
|         app: { | ||||
|           ...TEST_SETTINGS.app, | ||||
|           projectDirectory: projectDirName, | ||||
|         }, | ||||
|       }, | ||||
|     }) | ||||
|     const settingsOverrides = TOML.stringify( | ||||
|       appSettings | ||||
|         ? { settings: appSettings } | ||||
|         : { | ||||
|             ...TEST_SETTINGS, | ||||
|             settings: { | ||||
|               app: { | ||||
|                 ...TEST_SETTINGS.app, | ||||
|                 projectDirectory: projectDirName, | ||||
|               }, | ||||
|             }, | ||||
|           } | ||||
|     ) | ||||
|     await fsp.writeFile(tempSettingsFilePath, settingsOverrides) | ||||
|   } | ||||
|  | ||||
| @ -830,7 +965,7 @@ export async function setupElectron({ | ||||
|  | ||||
|   await setup(context, page) | ||||
|  | ||||
|   return { electronApp, page } | ||||
|   return { electronApp, page, dir: projectDirName } | ||||
| } | ||||
|  | ||||
| export async function isOutOfViewInScrollContainer( | ||||
| @ -851,3 +986,33 @@ export async function isOutOfViewInScrollContainer( | ||||
|  | ||||
|   return isOutOfView | ||||
| } | ||||
|  | ||||
| export async function createProjectAndRenameIt({ | ||||
|   name, | ||||
|   page, | ||||
| }: { | ||||
|   name: string | ||||
|   page: Page | ||||
| }) { | ||||
|   await page.getByRole('button', { name: 'New project' }).click() | ||||
|   await expect(page.getByText('Successfully created')).toBeVisible() | ||||
|   await expect(page.getByText('Successfully created')).not.toBeVisible() | ||||
|  | ||||
|   await expect(page.getByText(`project-000`)).toBeVisible() | ||||
|   await page.getByText(`project-000`).hover() | ||||
|   await page.getByText(`project-000`).focus() | ||||
|  | ||||
|   await page.getByLabel('sketch').first().click() | ||||
|  | ||||
|   await page.waitForTimeout(100) | ||||
|  | ||||
|   // type the name passed in | ||||
|   await page.keyboard.press('Backspace') | ||||
|   await page.keyboard.type(name) | ||||
|  | ||||
|   await page.getByLabel('checkmark').last().click() | ||||
| } | ||||
|  | ||||
| export function executorInputPath(fileName: string): string { | ||||
|   return join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName) | ||||
| } | ||||
|  | ||||
| @ -12,8 +12,8 @@ test.afterEach(async ({ page }, testInfo) => { | ||||
| }) | ||||
|  | ||||
| test.describe('Testing Camera Movement', () => { | ||||
|   test('Can moving camera', async ({ page, context }) => { | ||||
|     test.skip(process.platform === 'darwin', 'Can moving camera') | ||||
|   test('Can move camera reliably', async ({ page, context }) => { | ||||
|     test.skip(process.platform === 'darwin', 'Can move camera reliably') | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|  | ||||
| @ -102,6 +102,13 @@ test.describe('Testing Camera Movement', () => { | ||||
|     await bakeInRetries(async () => { | ||||
|       await page.mouse.move(700, 200) | ||||
|       await page.mouse.down({ button: 'right' }) | ||||
|       const appLogoBBox = await page.getByTestId('app-logo').boundingBox() | ||||
|       expect(appLogoBBox).not.toBeNull() | ||||
|       if (!appLogoBBox) throw new Error('app logo not found') | ||||
|       await page.mouse.move( | ||||
|         appLogoBBox.x + appLogoBBox.width / 2, | ||||
|         appLogoBBox.y + appLogoBBox.height / 2 | ||||
|       ) | ||||
|       await page.mouse.move(600, 303) | ||||
|       await page.mouse.up({ button: 'right' }) | ||||
|     }, [4, -10.5, -120]) | ||||
| @ -174,168 +181,166 @@ test.describe('Testing Camera Movement', () => { | ||||
|     }, [0, -85, -85]) | ||||
|   }) | ||||
|  | ||||
|   // TODO fixme something is wrong with sketch here | ||||
|   test.fixme( | ||||
|     'Zoom should be consistent when exiting or entering sketches', | ||||
|     async ({ page }) => { | ||||
|       // start new sketch pan and zoom before exiting, when exiting the sketch should stay in the same place | ||||
|       // than zoom and pan outside of sketch mode and enter again and it should not change from where it is | ||||
|       // than again for sketching | ||||
|   test('Zoom should be consistent when exiting or entering sketches', async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     // start new sketch pan and zoom before exiting, when exiting the sketch should stay in the same place | ||||
|     // than zoom and pan outside of sketch mode and enter again and it should not change from where it is | ||||
|     // than again for sketching | ||||
|  | ||||
|       test.skip(process.platform !== 'darwin', 'Zoom should be consistent') | ||||
|       const u = await getUtils(page) | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     test.skip(process.platform !== 'darwin', 'Zoom should be consistent') | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|  | ||||
|       await u.waitForAuthSkipAppStart() | ||||
|       await u.openDebugPanel() | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await u.openDebugPanel() | ||||
|  | ||||
|       await expect( | ||||
|         page.getByRole('button', { name: 'Start Sketch' }) | ||||
|       ).not.toBeDisabled() | ||||
|       await expect( | ||||
|         page.getByRole('button', { name: 'Start Sketch' }) | ||||
|       ).toBeVisible() | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Start Sketch' }) | ||||
|     ).not.toBeDisabled() | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Start Sketch' }) | ||||
|     ).toBeVisible() | ||||
|  | ||||
|       // click on "Start Sketch" button | ||||
|       await u.clearCommandLogs() | ||||
|       await page.getByRole('button', { name: 'Start Sketch' }).click() | ||||
|     // click on "Start Sketch" button | ||||
|     await u.clearCommandLogs() | ||||
|     await page.getByRole('button', { name: 'Start Sketch' }).click() | ||||
|     await page.waitForTimeout(100) | ||||
|  | ||||
|     // select a plane | ||||
|     await page.mouse.click(700, 325) | ||||
|  | ||||
|     let code = `const sketch001 = startSketchOn('XY')` | ||||
|     await expect(u.codeLocator).toHaveText(code) | ||||
|     await u.closeDebugPanel() | ||||
|  | ||||
|     await page.waitForTimeout(500) // TODO detect animation ending, or disable animation | ||||
|  | ||||
|     // move the camera slightly | ||||
|     await page.keyboard.down('Shift') | ||||
|     await page.mouse.move(700, 300) | ||||
|     await page.mouse.down({ button: 'right' }) | ||||
|     await page.mouse.move(800, 200) | ||||
|     await page.mouse.up({ button: 'right' }) | ||||
|     await page.keyboard.up('Shift') | ||||
|  | ||||
|     let y = 350, | ||||
|       x = 948 | ||||
|  | ||||
|     await u.canvasLocator.click({ position: { x: 783, y } }) | ||||
|     code += `\n  |> startProfileAt([8.12, -12.98], %)` | ||||
|     // await expect(u.codeLocator).toHaveText(code) | ||||
|     await u.canvasLocator.click({ position: { x, y } }) | ||||
|     code += `\n  |> line([11.18, 0], %)` | ||||
|     // await expect(u.codeLocator).toHaveText(code) | ||||
|     await u.canvasLocator.click({ position: { x, y: 275 } }) | ||||
|     code += `\n  |> line([0, 6.99], %)` | ||||
|     // await expect(u.codeLocator).toHaveText(code) | ||||
|  | ||||
|     // click the line button | ||||
|     await page.getByRole('button', { name: 'line Line', exact: true }).click() | ||||
|  | ||||
|     const hoverOverNothing = async () => { | ||||
|       // await u.canvasLocator.hover({position: {x: 700, y: 325}}) | ||||
|       await page.mouse.move(700, 325) | ||||
|       await page.waitForTimeout(100) | ||||
|  | ||||
|       // select a plane | ||||
|       await page.mouse.click(700, 325) | ||||
|  | ||||
|       let code = `const sketch001 = startSketchOn('XY')` | ||||
|       await expect(u.codeLocator).toHaveText(code) | ||||
|       await u.closeDebugPanel() | ||||
|  | ||||
|       await page.waitForTimeout(500) // TODO detect animation ending, or disable animation | ||||
|  | ||||
|       // move the camera slightly | ||||
|       await page.keyboard.down('Shift') | ||||
|       await page.mouse.move(700, 300) | ||||
|       await page.mouse.down({ button: 'right' }) | ||||
|       await page.mouse.move(800, 200) | ||||
|       await page.mouse.up({ button: 'right' }) | ||||
|       await page.keyboard.up('Shift') | ||||
|  | ||||
|       let y = 350, | ||||
|         x = 948 | ||||
|  | ||||
|       await u.canvasLocator.click({ position: { x: 783, y } }) | ||||
|       code += `\n  |> startProfileAt([8.12, -12.98], %)` | ||||
|       // await expect(u.codeLocator).toHaveText(code) | ||||
|       await u.canvasLocator.click({ position: { x, y } }) | ||||
|       code += `\n  |> line([11.18, 0], %)` | ||||
|       // await expect(u.codeLocator).toHaveText(code) | ||||
|       await u.canvasLocator.click({ position: { x, y: 275 } }) | ||||
|       code += `\n  |> line([0, 6.99], %)` | ||||
|       // await expect(u.codeLocator).toHaveText(code) | ||||
|  | ||||
|       // click the line button | ||||
|       await page.getByRole('button', { name: 'line Line', exact: true }).click() | ||||
|  | ||||
|       const hoverOverNothing = async () => { | ||||
|         // await u.canvasLocator.hover({position: {x: 700, y: 325}}) | ||||
|         await page.mouse.move(700, 325) | ||||
|         await page.waitForTimeout(100) | ||||
|         await expect(page.getByTestId('hover-highlight')).not.toBeVisible({ | ||||
|           timeout: 10_000, | ||||
|         }) | ||||
|       } | ||||
|  | ||||
|       await expect(page.getByTestId('hover-highlight')).not.toBeVisible() | ||||
|  | ||||
|       await page.waitForTimeout(200) | ||||
|       // hover over horizontal line | ||||
|       await u.canvasLocator.hover({ position: { x: 800, y } }) | ||||
|       await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|         timeout: 10_000, | ||||
|       }) | ||||
|       await page.waitForTimeout(200) | ||||
|  | ||||
|       await hoverOverNothing() | ||||
|       await page.waitForTimeout(200) | ||||
|       // hover over vertical line | ||||
|       await u.canvasLocator.hover({ position: { x, y: 325 } }) | ||||
|       await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|         timeout: 10_000, | ||||
|       }) | ||||
|  | ||||
|       await hoverOverNothing() | ||||
|  | ||||
|       // click exit sketch | ||||
|       await page.getByRole('button', { name: 'Exit Sketch' }).click() | ||||
|       await page.waitForTimeout(400) | ||||
|  | ||||
|       await hoverOverNothing() | ||||
|       await page.waitForTimeout(200) | ||||
|       // hover over horizontal line | ||||
|       await page.mouse.move(858, y, { steps: 5 }) | ||||
|       await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|         timeout: 10_000, | ||||
|       }) | ||||
|  | ||||
|       await hoverOverNothing() | ||||
|  | ||||
|       // hover over vertical line | ||||
|       await page.mouse.move(x, 325) | ||||
|       await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|         timeout: 10_000, | ||||
|       }) | ||||
|  | ||||
|       await hoverOverNothing() | ||||
|  | ||||
|       // hover over vertical line | ||||
|       await page.mouse.move(857, y) | ||||
|       await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|         timeout: 10_000, | ||||
|       }) | ||||
|       // now click it | ||||
|       await page.mouse.click(857, y) | ||||
|  | ||||
|       await expect( | ||||
|         page.getByRole('button', { name: 'Edit Sketch' }) | ||||
|       ).toBeVisible() | ||||
|       await page.getByRole('button', { name: 'Edit Sketch' }).click() | ||||
|  | ||||
|       await page.waitForTimeout(400) | ||||
|  | ||||
|       await hoverOverNothing() | ||||
|       x = 975 | ||||
|       y = 468 | ||||
|  | ||||
|       await page.waitForTimeout(100) | ||||
|       await page.mouse.move(x, 419, { steps: 5 }) | ||||
|       await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|         timeout: 10_000, | ||||
|       }) | ||||
|  | ||||
|       await hoverOverNothing() | ||||
|  | ||||
|       await page.mouse.move(855, y) | ||||
|       await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|         timeout: 10_000, | ||||
|       }) | ||||
|  | ||||
|       await hoverOverNothing() | ||||
|  | ||||
|       await page.getByRole('button', { name: 'Exit Sketch' }).click() | ||||
|       await page.waitForTimeout(200) | ||||
|  | ||||
|       await hoverOverNothing() | ||||
|       await page.waitForTimeout(200) | ||||
|  | ||||
|       await page.mouse.move(x, 419) | ||||
|       await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|         timeout: 10_000, | ||||
|       }) | ||||
|  | ||||
|       await hoverOverNothing() | ||||
|  | ||||
|       await page.mouse.move(855, y) | ||||
|       await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|       await expect(page.getByTestId('hover-highlight')).not.toBeVisible({ | ||||
|         timeout: 10_000, | ||||
|       }) | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|     await expect(page.getByTestId('hover-highlight')).not.toBeVisible() | ||||
|  | ||||
|     await page.waitForTimeout(200) | ||||
|     // hover over horizontal line | ||||
|     await u.canvasLocator.hover({ position: { x: 800, y } }) | ||||
|     await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|     await page.waitForTimeout(200) | ||||
|  | ||||
|     await hoverOverNothing() | ||||
|     await page.waitForTimeout(200) | ||||
|     // hover over vertical line | ||||
|     await u.canvasLocator.hover({ position: { x, y: 325 } }) | ||||
|     await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|  | ||||
|     await hoverOverNothing() | ||||
|  | ||||
|     // click exit sketch | ||||
|     await page.getByRole('button', { name: 'Exit Sketch' }).click() | ||||
|     await page.waitForTimeout(400) | ||||
|  | ||||
|     await hoverOverNothing() | ||||
|     await page.waitForTimeout(200) | ||||
|     // hover over horizontal line | ||||
|     await page.mouse.move(858, y, { steps: 5 }) | ||||
|     await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|  | ||||
|     await hoverOverNothing() | ||||
|  | ||||
|     // hover over vertical line | ||||
|     await page.mouse.move(x, 325) | ||||
|     await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|  | ||||
|     await hoverOverNothing() | ||||
|  | ||||
|     // hover over vertical line | ||||
|     await page.mouse.move(857, y) | ||||
|     await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|     // now click it | ||||
|     await page.mouse.click(857, y) | ||||
|  | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Edit Sketch' }) | ||||
|     ).toBeVisible() | ||||
|     await hoverOverNothing() | ||||
|     await page.getByRole('button', { name: 'Edit Sketch' }).click() | ||||
|  | ||||
|     await page.waitForTimeout(400) | ||||
|  | ||||
|     x = 975 | ||||
|     y = 468 | ||||
|  | ||||
|     await page.waitForTimeout(100) | ||||
|     await page.mouse.move(x, 419, { steps: 5 }) | ||||
|     await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|  | ||||
|     await hoverOverNothing() | ||||
|  | ||||
|     await page.mouse.move(855, y) | ||||
|     await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|  | ||||
|     await hoverOverNothing() | ||||
|  | ||||
|     await page.getByRole('button', { name: 'Exit Sketch' }).click() | ||||
|     await page.waitForTimeout(200) | ||||
|  | ||||
|     await hoverOverNothing() | ||||
|     await page.waitForTimeout(200) | ||||
|  | ||||
|     await page.mouse.move(x, 419) | ||||
|     await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|  | ||||
|     await hoverOverNothing() | ||||
|  | ||||
|     await page.mouse.move(855, y) | ||||
|     await expect(page.getByTestId('hover-highlight').first()).toBeVisible({ | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -31,8 +31,18 @@ test.describe('Testing selections', () => { | ||||
|  | ||||
|     const xAxisClick = () => | ||||
|       page.mouse.click(700, 253).then(() => page.waitForTimeout(100)) | ||||
|     const emptySpaceHover = () => | ||||
|       test.step('Hover over empty space', async () => { | ||||
|         await page.mouse.move(700, 143, { steps: 5 }) | ||||
|         await expect(page.locator('.hover-highlight')).not.toBeVisible() | ||||
|       }) | ||||
|     const emptySpaceClick = () => | ||||
|       page.mouse.click(700, 343).then(() => page.waitForTimeout(100)) | ||||
|       test.step(`Click in empty space`, async () => { | ||||
|         await page.mouse.click(700, 143) | ||||
|         await expect(page.locator('.cm-line').last()).toHaveClass( | ||||
|           /cm-activeLine/ | ||||
|         ) | ||||
|       }) | ||||
|     const topHorzSegmentClick = () => | ||||
|       page.mouse.click(709, 290).then(() => page.waitForTimeout(100)) | ||||
|     const bottomHorzSegmentClick = () => | ||||
| @ -171,7 +181,9 @@ test.describe('Testing selections', () => { | ||||
|       await emptySpaceClick() | ||||
|     } | ||||
|  | ||||
|     await selectionSequence() | ||||
|     await test.step(`Test hovering and selecting on fresh sketch`, async () => { | ||||
|       await selectionSequence() | ||||
|     }) | ||||
|  | ||||
|     // hovering in fresh sketch worked, lets try exiting and re-entering | ||||
|     await u.openAndClearDebugPanel() | ||||
| @ -184,16 +196,15 @@ test.describe('Testing selections', () => { | ||||
|  | ||||
|     // select a line, this verifies that sketches in the scene can be selected outside of sketch mode | ||||
|     await topHorzSegmentClick() | ||||
|     await page.waitForTimeout(100) | ||||
|     await emptySpaceHover() | ||||
|  | ||||
|     // enter sketch again | ||||
|     await u.doAndWaitForCmd( | ||||
|       () => page.getByRole('button', { name: 'Edit Sketch' }).click(), | ||||
|       'default_camera_get_settings' | ||||
|     ) | ||||
|     await page.waitForTimeout(150) | ||||
|  | ||||
|     await page.waitForTimeout(300) // wait for animation | ||||
|     await page.waitForTimeout(450) // wait for animation | ||||
|  | ||||
|     await u.openAndClearDebugPanel() | ||||
|     await u.sendCustomCmd({ | ||||
| @ -220,8 +231,9 @@ test.describe('Testing selections', () => { | ||||
|  | ||||
|     await u.closeDebugPanel() | ||||
|  | ||||
|     // hover again and check it works | ||||
|     await selectionSequence() | ||||
|     await test.step(`Test hovering and selecting on edited sketch`, async () => { | ||||
|       await selectionSequence() | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   test('Solids should be select and deletable', async ({ page }) => { | ||||
| @ -773,9 +785,9 @@ const extrude001 = extrude(50, sketch001) | ||||
|  | ||||
|     await page.waitForTimeout(1000) | ||||
|  | ||||
|     let noHoverColor: [number, number, number] = [82, 82, 82] | ||||
|     let hoverColor: [number, number, number] = [116, 116, 116] | ||||
|     let selectColor: [number, number, number] = [144, 148, 97] | ||||
|     let noHoverColor: [number, number, number] = [92, 92, 92] | ||||
|     let hoverColor: [number, number, number] = [127, 127, 127] | ||||
|     let selectColor: [number, number, number] = [155, 155, 105] | ||||
|  | ||||
|     const extrudeWall = { x: 670, y: 275 } | ||||
|     const extrudeText = `line([170.36, -121.61], %, $seg01)` | ||||
| @ -787,7 +799,7 @@ const extrude001 = extrude(50, sketch001) | ||||
|  | ||||
|     await expect | ||||
|       .poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor)) | ||||
|       .toBeLessThan(5) | ||||
|       .toBeLessThan(15) | ||||
|     await page.mouse.move(nothing.x, nothing.y) | ||||
|     await page.waitForTimeout(100) | ||||
|     await page.mouse.move(extrudeWall.x, extrudeWall.y) | ||||
| @ -798,43 +810,43 @@ const extrude001 = extrude(50, sketch001) | ||||
|     await page.waitForTimeout(200) | ||||
|     await expect( | ||||
|       await u.getGreatestPixDiff(extrudeWall, hoverColor) | ||||
|     ).toBeLessThan(6) | ||||
|     ).toBeLessThan(15) | ||||
|     await page.mouse.click(extrudeWall.x, extrudeWall.y) | ||||
|     await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`) | ||||
|     await page.waitForTimeout(200) | ||||
|     await expect( | ||||
|       await u.getGreatestPixDiff(extrudeWall, selectColor) | ||||
|     ).toBeLessThan(6) | ||||
|     ).toBeLessThan(15) | ||||
|     await page.waitForTimeout(1000) | ||||
|     // check color stays there, i.e. not overridden (this was a bug previously) | ||||
|     await expect( | ||||
|       await u.getGreatestPixDiff(extrudeWall, selectColor) | ||||
|     ).toBeLessThan(6) | ||||
|     ).toBeLessThan(15) | ||||
|  | ||||
|     await page.mouse.move(nothing.x, nothing.y) | ||||
|     await page.waitForTimeout(300) | ||||
|     await expect(page.getByTestId('hover-highlight')).not.toBeVisible() | ||||
|  | ||||
|     // because of shading, color is not exact everywhere on the face | ||||
|     noHoverColor = [104, 104, 104] | ||||
|     hoverColor = [134, 134, 134] | ||||
|     selectColor = [158, 162, 110] | ||||
|     noHoverColor = [115, 115, 115] | ||||
|     hoverColor = [145, 145, 145] | ||||
|     selectColor = [168, 168, 120] | ||||
|  | ||||
|     await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(6) | ||||
|     await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(15) | ||||
|     await page.mouse.move(cap.x, cap.y) | ||||
|     await expect(page.getByTestId('hover-highlight').first()).toBeVisible() | ||||
|     await expect(page.getByTestId('hover-highlight').first()).toContainText( | ||||
|       removeAfterFirstParenthesis(capText) | ||||
|     ) | ||||
|     await page.waitForTimeout(200) | ||||
|     await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(6) | ||||
|     await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15) | ||||
|     await page.mouse.click(cap.x, cap.y) | ||||
|     await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`) | ||||
|     await page.waitForTimeout(200) | ||||
|     await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6) | ||||
|     await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15) | ||||
|     await page.waitForTimeout(1000) | ||||
|     // check color stays there, i.e. not overridden (this was a bug previously) | ||||
|     await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6) | ||||
|     await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15) | ||||
|   }) | ||||
|   test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({ | ||||
|     page, | ||||
|  | ||||
| @ -1,8 +1,19 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
| import { getUtils, setup, tearDown, setupElectron } from './test-utils' | ||||
| import * as fsp from 'fs/promises' | ||||
| import { SaveSettingsPayload } from 'lib/settings/settingsTypes' | ||||
| import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates' | ||||
| import { join } from 'path' | ||||
| import { | ||||
|   getUtils, | ||||
|   setup, | ||||
|   setupElectron, | ||||
|   tearDown, | ||||
|   executorInputPath, | ||||
| } from './test-utils' | ||||
| import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes' | ||||
| import { | ||||
|   TEST_SETTINGS_KEY, | ||||
|   TEST_SETTINGS_CORRUPTED, | ||||
|   TEST_SETTINGS, | ||||
| } from './storageStates' | ||||
| import * as TOML from '@iarna/toml' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }) => { | ||||
| @ -72,7 +83,7 @@ test.describe('Testing settings', () => { | ||||
|     const inputLocator = page.locator('input[name="modeling-showDebugPanel"]') | ||||
|  | ||||
|     // Open the settings modal with the browser keyboard shortcut | ||||
|     await page.keyboard.press('Meta+Shift+,') | ||||
|     await page.keyboard.press('ControlOrMeta+Shift+,') | ||||
|  | ||||
|     await expect(headingLocator).toBeVisible() | ||||
|     await page.locator('#showDebugPanel').getByText('OffOn').click() | ||||
| @ -82,7 +93,7 @@ test.describe('Testing settings', () => { | ||||
|     await test.step('Open settings with keyboard shortcut', async () => { | ||||
|       await page.getByTestId('settings-close-button').click() | ||||
|       await page.locator('.cm-content').click() | ||||
|       await page.keyboard.press('Meta+Shift+,') | ||||
|       await page.keyboard.press('ControlOrMeta+Shift+,') | ||||
|       await expect(headingLocator).toBeVisible() | ||||
|     }) | ||||
|  | ||||
| @ -115,31 +126,65 @@ test.describe('Testing settings', () => { | ||||
|     ).not.toBeChecked() | ||||
|   }) | ||||
|  | ||||
|   test('Project and user settings can be reset', async ({ page }) => { | ||||
|   test('Keybindings display the correct hotkey for Command Palette', async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|     await u.waitForAuthSkipAppStart() | ||||
|     await page | ||||
|       .getByRole('button', { name: 'Start Sketch' }) | ||||
|       .waitFor({ state: 'visible' }) | ||||
|  | ||||
|     await test.step('Open keybindings settings', async () => { | ||||
|       // Open the settings modal with the browser keyboard shortcut | ||||
|       await page.keyboard.press('ControlOrMeta+Shift+,') | ||||
|  | ||||
|       // Go to Keybindings tab. | ||||
|       const keybindingsTab = page.getByRole('radio', { name: 'Keybindings' }) | ||||
|       await keybindingsTab.click() | ||||
|     }) | ||||
|  | ||||
|     // Go to the hotkey for Command Palette. | ||||
|     const commandPalette = page.getByText('Toggle Command Palette') | ||||
|     await commandPalette.scrollIntoViewIfNeeded() | ||||
|  | ||||
|     // The heading is above it and should be in view now. | ||||
|     const commandPaletteHeading = page.getByRole('heading', { | ||||
|       name: 'Command Palette', | ||||
|     }) | ||||
|     // The hotkey is in a kbd element next to the heading. | ||||
|     const hotkey = commandPaletteHeading.locator('+ div kbd') | ||||
|     const text = process.platform === 'darwin' ? 'Command+K' : 'Control+K' | ||||
|     await expect(hotkey).toHaveText(text) | ||||
|   }) | ||||
|  | ||||
|   test('Project and user settings can be reset', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await test.step(`Setup`, async () => { | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       await u.waitForAuthSkipAppStart() | ||||
|     }) | ||||
|  | ||||
|     // Selectors and constants | ||||
|     const projectSettingsTab = page.getByRole('radio', { name: 'Project' }) | ||||
|     const userSettingsTab = page.getByRole('radio', { name: 'User' }) | ||||
|     const resetButton = page.getByRole('button', { | ||||
|       name: 'Restore default settings', | ||||
|     }) | ||||
|     const resetButton = (level: SettingsLevel) => | ||||
|       page.getByRole('button', { | ||||
|         name: `Reset ${level}-level settings`, | ||||
|       }) | ||||
|     const themeColorSetting = page.locator('#themeColor').getByRole('slider') | ||||
|     const settingValues = { | ||||
|       default: '259', | ||||
|       user: '120', | ||||
|       project: '50', | ||||
|     } | ||||
|     const resetToast = (level: SettingsLevel) => | ||||
|       page.getByText(`${level}-level settings were reset`) | ||||
|  | ||||
|     // Open the settings modal with lower-right button | ||||
|     await page.getByRole('link', { name: 'Settings' }).last().click() | ||||
|     await expect( | ||||
|       page.getByRole('heading', { name: 'Settings', exact: true }) | ||||
|     ).toBeVisible() | ||||
|     await test.step(`Open the settings modal`, async () => { | ||||
|       await page.getByRole('link', { name: 'Settings' }).last().click() | ||||
|       await expect( | ||||
|         page.getByRole('heading', { name: 'Settings', exact: true }) | ||||
|       ).toBeVisible() | ||||
|     }) | ||||
|  | ||||
|     await test.step('Set up theme color', async () => { | ||||
|       // Verify we're looking at the project-level settings, | ||||
| @ -158,37 +203,40 @@ test.describe('Testing settings', () => { | ||||
|  | ||||
|     await test.step('Reset project settings', async () => { | ||||
|       // Click the reset settings button. | ||||
|       await resetButton.click() | ||||
|       await resetButton('project').click() | ||||
|  | ||||
|       await expect(page.getByText('Settings restored to default')).toBeVisible() | ||||
|       await expect( | ||||
|         page.getByText('Settings restored to default') | ||||
|       ).not.toBeVisible() | ||||
|       await expect(resetToast('project')).toBeVisible() | ||||
|       await expect(resetToast('project')).not.toBeVisible() | ||||
|  | ||||
|       // Verify it is now set to the inherited user value | ||||
|       await expect(themeColorSetting).toHaveValue(settingValues.default) | ||||
|       await expect(themeColorSetting).toHaveValue(settingValues.user) | ||||
|  | ||||
|       // Check that the user setting also rolled back | ||||
|       await userSettingsTab.click() | ||||
|       await expect(themeColorSetting).toHaveValue(settingValues.default) | ||||
|       await projectSettingsTab.click() | ||||
|       await test.step(`Check that the user settings did not change`, async () => { | ||||
|         await userSettingsTab.click() | ||||
|         await expect(themeColorSetting).toHaveValue(settingValues.user) | ||||
|       }) | ||||
|  | ||||
|       // Set project-level value to 50 again to test the user-level reset | ||||
|       await themeColorSetting.fill(settingValues.project) | ||||
|       await userSettingsTab.click() | ||||
|       await test.step(`Set project-level again to test the user-level reset`, async () => { | ||||
|         await projectSettingsTab.click() | ||||
|         await themeColorSetting.fill(settingValues.project) | ||||
|         await userSettingsTab.click() | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     await test.step('Reset user settings', async () => { | ||||
|       // Change the setting and click the reset settings button. | ||||
|       await themeColorSetting.fill(settingValues.user) | ||||
|       await resetButton.click() | ||||
|       // Click the reset settings button. | ||||
|       await resetButton('user').click() | ||||
|  | ||||
|       await expect(resetToast('user')).toBeVisible() | ||||
|       await expect(resetToast('user')).not.toBeVisible() | ||||
|  | ||||
|       // Verify it is now set to the default value | ||||
|       await expect(themeColorSetting).toHaveValue(settingValues.default) | ||||
|  | ||||
|       // Check that the project setting also changed | ||||
|       await projectSettingsTab.click() | ||||
|       await expect(themeColorSetting).toHaveValue(settingValues.default) | ||||
|       await test.step(`Check that the project settings did not change`, async () => { | ||||
|         await projectSettingsTab.click() | ||||
|         await expect(themeColorSetting).toHaveValue(settingValues.project) | ||||
|       }) | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
| @ -203,10 +251,11 @@ test.describe('Testing settings', () => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async (dir) => { | ||||
|           await fsp.mkdir(`${dir}/bracket`, { recursive: true }) | ||||
|           const bracketDir = join(dir, 'bracket') | ||||
|           await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|           await fsp.copyFile( | ||||
|             'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', | ||||
|             `${dir}/bracket/main.kcl` | ||||
|             executorInputPath('focusrite_scarlett_mounting_braket.kcl'), | ||||
|             join(bracketDir, 'main.kcl') | ||||
|           ) | ||||
|         }, | ||||
|       }) | ||||
| @ -250,7 +299,7 @@ test.describe('Testing settings', () => { | ||||
|       }) | ||||
|  | ||||
|       await test.step('Refresh the application and see project setting applied', async () => { | ||||
|         await page.reload() | ||||
|         await page.reload({ waitUntil: 'domcontentloaded' }) | ||||
|  | ||||
|         await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor) | ||||
|         await settingsCloseButton.click() | ||||
| @ -264,4 +313,410 @@ test.describe('Testing settings', () => { | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     `Load desktop app with no settings file`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         // This is what makes no settings file get created | ||||
|         cleanProjectDir: false, | ||||
|         testInfo, | ||||
|       }) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|  | ||||
|       // Selectors and constants | ||||
|       const errorHeading = page.getByRole('heading', { | ||||
|         name: 'An unextected error occurred', | ||||
|       }) | ||||
|       const projectDirLink = page.getByText('Loaded from') | ||||
|  | ||||
|       // If the app loads without exploding we're in the clear | ||||
|       await expect(errorHeading).not.toBeVisible() | ||||
|       await expect(projectDirLink).toBeVisible() | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     `Load desktop app with a settings file, but no project directory setting`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         appSettings: { | ||||
|           app: { | ||||
|             themeColor: '259', | ||||
|           }, | ||||
|         }, | ||||
|       }) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|  | ||||
|       // Selectors and constants | ||||
|       const errorHeading = page.getByRole('heading', { | ||||
|         name: 'An unextected error occurred', | ||||
|       }) | ||||
|       const projectDirLink = page.getByText('Loaded from') | ||||
|  | ||||
|       // If the app loads without exploding we're in the clear | ||||
|       await expect(errorHeading).not.toBeVisible() | ||||
|       await expect(projectDirLink).toBeVisible() | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     `Closing settings modal should go back to the original file being viewed`, | ||||
|     { tag: '@electron' }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async (dir) => { | ||||
|           const bracketDir = join(dir, 'project-000') | ||||
|           await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|           await fsp.copyFile( | ||||
|             executorInputPath('cube.kcl'), | ||||
|             join(bracketDir, 'main.kcl') | ||||
|           ) | ||||
|           await fsp.copyFile( | ||||
|             executorInputPath('cylinder.kcl'), | ||||
|             join(bracketDir, '2.kcl') | ||||
|           ) | ||||
|         }, | ||||
|       }) | ||||
|       const kclCube = await fsp.readFile(executorInputPath('cube.kcl'), 'utf-8') | ||||
|       const kclCylinder = await fsp.readFile( | ||||
|         executorInputPath('cylinder.kcl'), | ||||
|         'utf8' | ||||
|       ) | ||||
|  | ||||
|       const { | ||||
|         openKclCodePanel, | ||||
|         openFilePanel, | ||||
|         waitForPageLoad, | ||||
|         selectFile, | ||||
|         editorTextMatches, | ||||
|       } = await getUtils(page, test) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       page.on('console', console.log) | ||||
|  | ||||
|       await test.step('Precondition: Open to second project file', async () => { | ||||
|         await expect(page.getByTestId('home-section')).toBeVisible() | ||||
|         await page.getByText('project-000').click() | ||||
|         await waitForPageLoad() | ||||
|         await openKclCodePanel() | ||||
|         await openFilePanel() | ||||
|         await editorTextMatches(kclCube) | ||||
|  | ||||
|         await selectFile('2.kcl') | ||||
|         await editorTextMatches(kclCylinder) | ||||
|       }) | ||||
|  | ||||
|       const settingsOpenButton = page.getByRole('link', { | ||||
|         name: 'settings Settings', | ||||
|       }) | ||||
|       const settingsCloseButton = page.getByTestId('settings-close-button') | ||||
|  | ||||
|       await test.step('Open and close settings', async () => { | ||||
|         await settingsOpenButton.click() | ||||
|         await expect( | ||||
|           page.getByRole('heading', { name: 'Settings', exact: true }) | ||||
|         ).toBeVisible() | ||||
|         await settingsCloseButton.click() | ||||
|       }) | ||||
|  | ||||
|       await test.step('Postcondition: Same file content is in editor as before settings opened', async () => { | ||||
|         await editorTextMatches(kclCylinder) | ||||
|       }) | ||||
|  | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test('Changing modeling default unit', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await test.step(`Test setup`, async () => { | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       await u.waitForAuthSkipAppStart() | ||||
|       await page | ||||
|         .getByRole('button', { name: 'Start Sketch' }) | ||||
|         .waitFor({ state: 'visible' }) | ||||
|     }) | ||||
|  | ||||
|     // Selectors and constants | ||||
|     const userSettingsTab = page.getByRole('radio', { name: 'User' }) | ||||
|     const projectSettingsTab = page.getByRole('radio', { name: 'Project' }) | ||||
|     const defaultUnitSection = page.getByText( | ||||
|       'default unitRoll back default unitRoll back to match' | ||||
|     ) | ||||
|     const defaultUnitRollbackButton = page.getByRole('button', { | ||||
|       name: 'Roll back default unit', | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Open the settings modal`, async () => { | ||||
|       await page.getByRole('link', { name: 'Settings' }).last().click() | ||||
|       await expect( | ||||
|         page.getByRole('heading', { name: 'Settings', exact: true }) | ||||
|       ).toBeVisible() | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Reset unit setting`, async () => { | ||||
|       await userSettingsTab.click() | ||||
|       await defaultUnitSection.hover() | ||||
|       await defaultUnitRollbackButton.click() | ||||
|       await projectSettingsTab.click() | ||||
|     }) | ||||
|  | ||||
|     await test.step('Change modeling default unit within project tab', async () => { | ||||
|       const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => { | ||||
|         await test.step(`Set modeling default unit to ${unitOfMeasure}`, async () => { | ||||
|           await page | ||||
|             .getByTestId('modeling-defaultUnit') | ||||
|             .selectOption(`${unitOfMeasure}`) | ||||
|           const toastMessage = page.getByText( | ||||
|             `Set default unit to "${unitOfMeasure}" for this project` | ||||
|           ) | ||||
|           await expect(toastMessage).toBeVisible() | ||||
|         }) | ||||
|       } | ||||
|       await changeUnitOfMeasureInProjectTab('in') | ||||
|       await changeUnitOfMeasureInProjectTab('ft') | ||||
|       await changeUnitOfMeasureInProjectTab('yd') | ||||
|       await changeUnitOfMeasureInProjectTab('mm') | ||||
|       await changeUnitOfMeasureInProjectTab('cm') | ||||
|       await changeUnitOfMeasureInProjectTab('m') | ||||
|     }) | ||||
|  | ||||
|     // Go to the user tab | ||||
|     await userSettingsTab.click() | ||||
|     await test.step('Change modeling default unit within user tab', async () => { | ||||
|       const changeUnitOfMeasureInUserTab = async (unitOfMeasure: string) => { | ||||
|         await test.step(`Set modeling default unit to ${unitOfMeasure}`, async () => { | ||||
|           await page | ||||
|             .getByTestId('modeling-defaultUnit') | ||||
|             .selectOption(`${unitOfMeasure}`) | ||||
|           const toastMessage = page.getByText( | ||||
|             `Set default unit to "${unitOfMeasure}" as a user default` | ||||
|           ) | ||||
|           await expect(toastMessage).toBeVisible() | ||||
|         }) | ||||
|       } | ||||
|       await changeUnitOfMeasureInUserTab('in') | ||||
|       await changeUnitOfMeasureInUserTab('ft') | ||||
|       await changeUnitOfMeasureInUserTab('yd') | ||||
|       await changeUnitOfMeasureInUserTab('mm') | ||||
|       await changeUnitOfMeasureInUserTab('cm') | ||||
|       await changeUnitOfMeasureInUserTab('m') | ||||
|     }) | ||||
|  | ||||
|     // Close settings | ||||
|     const settingsCloseButton = page.getByTestId('settings-close-button') | ||||
|     await settingsCloseButton.click() | ||||
|  | ||||
|     await test.step('Change modeling default unit within command bar', async () => { | ||||
|       const commands = page.getByRole('button', { name: 'Commands' }) | ||||
|       const changeUnitOfMeasureInCommandBar = async (unitOfMeasure: string) => { | ||||
|         // Open command bar | ||||
|         await commands.click() | ||||
|         const settingsModelingDefaultUnitCommand = page.getByText( | ||||
|           'Settings · modeling · default unit' | ||||
|         ) | ||||
|         await settingsModelingDefaultUnitCommand.click() | ||||
|  | ||||
|         const commandOption = page.getByRole('option', { | ||||
|           name: unitOfMeasure, | ||||
|           exact: true, | ||||
|         }) | ||||
|         await commandOption.click() | ||||
|  | ||||
|         const toastMessage = page.getByText( | ||||
|           `Set default unit to "${unitOfMeasure}" for this project` | ||||
|         ) | ||||
|         await expect(toastMessage).toBeVisible() | ||||
|       } | ||||
|       await changeUnitOfMeasureInCommandBar('in') | ||||
|       await changeUnitOfMeasureInCommandBar('ft') | ||||
|       await changeUnitOfMeasureInCommandBar('yd') | ||||
|       await changeUnitOfMeasureInCommandBar('mm') | ||||
|       await changeUnitOfMeasureInCommandBar('cm') | ||||
|       await changeUnitOfMeasureInCommandBar('m') | ||||
|     }) | ||||
|  | ||||
|     await test.step('Change modeling default unit within gizmo', async () => { | ||||
|       const changeUnitOfMeasureInGizmo = async ( | ||||
|         unitOfMeasure: string, | ||||
|         copy: string | ||||
|       ) => { | ||||
|         const gizmo = page.getByRole('button', { | ||||
|           name: 'Current units are: ', | ||||
|         }) | ||||
|         await gizmo.click() | ||||
|         const button = page.getByRole('button', { | ||||
|           name: copy, | ||||
|           exact: true, | ||||
|         }) | ||||
|         await button.click() | ||||
|         const toastMessage = page.getByText( | ||||
|           `Set default unit to "${unitOfMeasure}" for this project` | ||||
|         ) | ||||
|         await expect(toastMessage).toBeVisible() | ||||
|       } | ||||
|  | ||||
|       await changeUnitOfMeasureInGizmo('in', 'Inches') | ||||
|       await changeUnitOfMeasureInGizmo('ft', 'Feet') | ||||
|       await changeUnitOfMeasureInGizmo('yd', 'Yards') | ||||
|       await changeUnitOfMeasureInGizmo('mm', 'Millimeters') | ||||
|       await changeUnitOfMeasureInGizmo('cm', 'Centimeters') | ||||
|       await changeUnitOfMeasureInGizmo('m', 'Meters') | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   test('Changing theme in sketch mode', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
|     await page.addInitScript(() => { | ||||
|       localStorage.setItem( | ||||
|         'persistCode', | ||||
|         `const sketch001 = startSketchOn('XZ') | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> line([5, 0], %) | ||||
|   |> line([0, 5], %) | ||||
|   |> line([-5, 0], %) | ||||
|   |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
|   |> close(%) | ||||
| const extrude001 = extrude(5, sketch001) | ||||
| ` | ||||
|       ) | ||||
|     }) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|  | ||||
|     // Selectors and constants | ||||
|     const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' }) | ||||
|     const lineToolButton = page.getByTestId('line') | ||||
|     const segmentOverlays = page.getByTestId('segment-overlay') | ||||
|     const sketchOriginLocation = { x: 600, y: 250 } | ||||
|     const darkThemeSegmentColor: [number, number, number] = [215, 215, 215] | ||||
|     const lightThemeSegmentColor: [number, number, number] = [90, 90, 90] | ||||
|  | ||||
|     await test.step(`Get into sketch mode`, async () => { | ||||
|       await u.waitForAuthSkipAppStart() | ||||
|       await page.mouse.click(700, 200) | ||||
|       await expect(editSketchButton).toBeVisible() | ||||
|       await editSketchButton.click() | ||||
|  | ||||
|       // We use the line tool as a proxy for sketch mode | ||||
|       await expect(lineToolButton).toBeVisible() | ||||
|       await expect(segmentOverlays).toHaveCount(4) | ||||
|       // but we allow more time to pass for animating to the sketch | ||||
|       await page.waitForTimeout(1000) | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Check the sketch line color before`, async () => { | ||||
|       await expect | ||||
|         .poll(() => | ||||
|           u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor) | ||||
|         ) | ||||
|         .toBeLessThan(15) | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Change theme to light using command palette`, async () => { | ||||
|       await page.keyboard.press('ControlOrMeta+K') | ||||
|       await page.getByRole('option', { name: 'theme' }).click() | ||||
|       await page.getByRole('option', { name: 'light' }).click() | ||||
|       await expect(page.getByText('theme to "light"')).toBeVisible() | ||||
|  | ||||
|       // Make sure we haven't left sketch mode | ||||
|       await expect(lineToolButton).toBeVisible() | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Check the sketch line color after`, async () => { | ||||
|       await expect | ||||
|         .poll(() => | ||||
|           u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor) | ||||
|         ) | ||||
|         .toBeLessThan(15) | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   test(`Turning off "Show debug panel" with debug panel open leaves no phantom panel`, async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     const u = await getUtils(page) | ||||
|  | ||||
|     // Override beforeEach test setup | ||||
|     // with debug panel open | ||||
|     // but "show debug panel" set to false | ||||
|     await page.addInitScript( | ||||
|       async ({ settingsKey, settings }) => { | ||||
|         localStorage.setItem(settingsKey, settings) | ||||
|         localStorage.setItem( | ||||
|           'persistModelingContext', | ||||
|           '{"openPanes":["debug"]}' | ||||
|         ) | ||||
|       }, | ||||
|       { | ||||
|         settingsKey: TEST_SETTINGS_KEY, | ||||
|         settings: TOML.stringify({ | ||||
|           settings: { | ||||
|             ...TEST_SETTINGS, | ||||
|             modeling: { ...TEST_SETTINGS.modeling, showDebugPanel: false }, | ||||
|           }, | ||||
|         }), | ||||
|       } | ||||
|     ) | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|  | ||||
|     // Constants and locators | ||||
|     const resizeHandle = page.locator('.sidebar-resize-handles > div.block') | ||||
|     const debugPaneButton = page.getByTestId('debug-pane-button') | ||||
|     const commandsButton = page.getByRole('button', { name: 'Commands' }) | ||||
|     const debugPaneOption = page.getByRole('option', { | ||||
|       name: 'Settings · modeling · show debug panel', | ||||
|     }) | ||||
|  | ||||
|     async function setShowDebugPanelTo(value: 'On' | 'Off') { | ||||
|       await commandsButton.click() | ||||
|       await debugPaneOption.click() | ||||
|       await page.getByRole('option', { name: value }).click() | ||||
|       await expect( | ||||
|         page.getByText( | ||||
|           `Set show debug panel to "${value === 'On'}" for this project` | ||||
|         ) | ||||
|       ).toBeVisible() | ||||
|     } | ||||
|  | ||||
|     await test.step(`Initial load with corrupted settings`, async () => { | ||||
|       await u.waitForAuthSkipAppStart() | ||||
|       // Check that the debug panel is not visible | ||||
|       await expect(debugPaneButton).not.toBeVisible() | ||||
|       // Check the pane resize handle wrapper is not visible | ||||
|       await expect(resizeHandle).not.toBeVisible() | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Open code pane to verify we see the resize handles`, async () => { | ||||
|       await u.openKclCodePanel() | ||||
|       await expect(resizeHandle).toBeVisible() | ||||
|       await u.closeKclCodePanel() | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Turn on debug panel, open it`, async () => { | ||||
|       await setShowDebugPanelTo('On') | ||||
|       await expect(debugPaneButton).toBeVisible() | ||||
|       // We want the logic to clear the phantom panel, so we shouldn't see | ||||
|       // the real panel (and therefore the resize handle) yet | ||||
|       await expect(resizeHandle).not.toBeVisible() | ||||
|       await u.openDebugPanel() | ||||
|       await expect(resizeHandle).toBeVisible() | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Turn off debug panel setting with it open`, async () => { | ||||
|       await setShowDebugPanelTo('Off') | ||||
|       await expect(debugPaneButton).not.toBeVisible() | ||||
|       await expect(resizeHandle).not.toBeVisible() | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| import { test, expect, Page } from '@playwright/test' | ||||
| import { getUtils, setup, tearDown } from './test-utils' | ||||
| import { getUtils, setup, tearDown, setupElectron } from './test-utils' | ||||
| import { join } from 'path' | ||||
| import fs from 'fs' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }) => { | ||||
|   await setup(context, page) | ||||
| @ -9,8 +11,6 @@ test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
|  | ||||
| const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control' | ||||
|  | ||||
| test.describe('Text-to-CAD tests', () => { | ||||
|   test('basic lego happy case', async ({ page }) => { | ||||
|     const u = await getUtils(page) | ||||
| @ -298,9 +298,9 @@ test.describe('Text-to-CAD tests', () => { | ||||
|     await expect(page.locator('textarea')).toContainText(badPrompt) | ||||
|  | ||||
|     // Select all and start a new prompt. | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.press('KeyA') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|     await page.keyboard.type('a 2x4 lego') | ||||
|  | ||||
|     // Submit the new prompt. | ||||
| @ -520,9 +520,9 @@ test.describe('Text-to-CAD tests', () => { | ||||
|     await page.locator('.cm-content').click({ position: { x: 10, y: 10 } }) | ||||
|  | ||||
|     // Paste the code. | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.press('KeyV') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|  | ||||
|     // Expect the code to be pasted. | ||||
|     await expect(page.locator('.cm-content')).toContainText(`2x8`) | ||||
| @ -534,7 +534,7 @@ test.describe('Text-to-CAD tests', () => { | ||||
|  | ||||
|     // Ensure the final toast remains. | ||||
|     await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible() | ||||
|     await expect(page.getByText(`a 2x8 lego`)).not.toBeVisible() | ||||
|     await expect(page.getByText(`Prompt: "a 2x8 lego`)).not.toBeVisible() | ||||
|     await expect(page.getByText(`a 2x4 lego`)).toBeVisible() | ||||
|  | ||||
|     // Ensure you can copy the code for the final model. | ||||
| @ -549,13 +549,13 @@ test.describe('Text-to-CAD tests', () => { | ||||
|     await page.locator('.cm-content').click({ position: { x: 10, y: 10 } }) | ||||
|  | ||||
|     // Paste the code. | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.press('KeyA') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|     await page.keyboard.press('Backspace') | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.press('KeyV') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|  | ||||
|     // Expect the code to be pasted. | ||||
|     await expect(page.locator('.cm-content')).toContainText(`2x4`) | ||||
| @ -636,9 +636,9 @@ test.describe('Text-to-CAD tests', () => { | ||||
|     await page.locator('.cm-content').click({ position: { x: 10, y: 10 } }) | ||||
|  | ||||
|     // Paste the code. | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.press('KeyV') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|  | ||||
|     // Expect the code to be pasted. | ||||
|     await expect(page.locator('.cm-content')).toContainText(`2x4`) | ||||
| @ -685,3 +685,75 @@ async function sendPromptFromCommandBar(page: Page, promptStr: string) { | ||||
|     await page.keyboard.press('Enter') | ||||
|   }) | ||||
| } | ||||
|  | ||||
| test( | ||||
|   'Text-to-CAD functionality', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     const projectName = 'project-000' | ||||
|     const prompt = 'lego 2x4' | ||||
|     const textToCadFileName = 'lego-2x4.kcl' | ||||
|  | ||||
|     const { electronApp, page, dir } = await setupElectron({ testInfo }) | ||||
|     const fileExists = () => | ||||
|       fs.existsSync(join(dir, projectName, textToCadFileName)) | ||||
|  | ||||
|     const { | ||||
|       createAndSelectProject, | ||||
|       openFilePanel, | ||||
|       openKclCodePanel, | ||||
|       waitForPageLoad, | ||||
|     } = await getUtils(page, test) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|  | ||||
|     // Locators | ||||
|     const projectMenuButton = page.getByRole('button', { name: projectName }) | ||||
|     const textToCadFileButton = page.getByRole('listitem').filter({ | ||||
|       has: page.getByRole('button', { name: textToCadFileName }), | ||||
|     }) | ||||
|     const textToCadComment = page.getByText( | ||||
|       `// Generated by Text-to-CAD: ${prompt}` | ||||
|     ) | ||||
|  | ||||
|     // Create and navigate to the project | ||||
|     await createAndSelectProject('project-000') | ||||
|  | ||||
|     // Wait for Start Sketch otherwise you will not have access Text-to-CAD command | ||||
|     await waitForPageLoad() | ||||
|     await openFilePanel() | ||||
|     await openKclCodePanel() | ||||
|  | ||||
|     await test.step(`Test file creation`, async () => { | ||||
|       await sendPromptFromCommandBar(page, prompt) | ||||
|       // File is considered created if it shows up in the Project Files pane | ||||
|       await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 }) | ||||
|       expect(fileExists()).toBeTruthy() | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Test file navigation`, async () => { | ||||
|       await expect(projectMenuButton).toContainText('main.kcl') | ||||
|       await textToCadFileButton.click() | ||||
|       // File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane | ||||
|       await expect(textToCadComment).toBeVisible({ timeout: 20_000 }) | ||||
|       await expect(projectMenuButton).toContainText(textToCadFileName) | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Test file deletion on rejection`, async () => { | ||||
|       const rejectButton = page.getByRole('button', { name: 'Reject' }) | ||||
|       // A file is created and can be navigated to while this prompt is still opened | ||||
|       // Click the "Reject" button within the prompt and it will delete the file. | ||||
|       await rejectButton.click() | ||||
|  | ||||
|       const submittingToastMessage = page.getByText( | ||||
|         `Successfully deleted file "lego-2x4.kcl"` | ||||
|       ) | ||||
|       await expect(submittingToastMessage).toBeVisible() | ||||
|       expect(fileExists()).toBeFalsy() | ||||
|       // Confirm we've navigated back to the main.kcl file after deletion | ||||
|       await expect(projectMenuButton).toContainText('main.kcl') | ||||
|     }) | ||||
|  | ||||
|     await electronApp.close() | ||||
|   } | ||||
| ) | ||||
|  | ||||
| @ -1,13 +1,6 @@ | ||||
| import { test, expect } from '@playwright/test' | ||||
|  | ||||
| import { | ||||
|   doExport, | ||||
|   getUtils, | ||||
|   makeTemplate, | ||||
|   metaModifier, | ||||
|   setup, | ||||
|   tearDown, | ||||
| } from './test-utils' | ||||
| import { doExport, getUtils, makeTemplate, setup, tearDown } from './test-utils' | ||||
|  | ||||
| test.beforeEach(async ({ context, page }) => { | ||||
|   await setup(context, page) | ||||
| @ -17,8 +10,6 @@ test.afterEach(async ({ page }, testInfo) => { | ||||
|   await tearDown(page, testInfo) | ||||
| }) | ||||
|  | ||||
| const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control' | ||||
|  | ||||
| test('Units menu', async ({ page }) => { | ||||
|   const u = await getUtils(page) | ||||
|   await page.setViewportSize({ width: 1200, height: 500 }) | ||||
| @ -157,7 +148,7 @@ test('Paste should not work unless an input is focused', async ({ | ||||
|  | ||||
|   // Paste without the code pane focused | ||||
|   await codeEditorText.blur() | ||||
|   await page.keyboard.press(`${metaModifier}+KeyV`) | ||||
|   await page.keyboard.press('ControlOrMeta+KeyV') | ||||
|  | ||||
|   // Show that the paste didn't work but typing did | ||||
|   await expect(codeEditorText).not.toContainText(pasteContent) | ||||
| @ -166,7 +157,7 @@ test('Paste should not work unless an input is focused', async ({ | ||||
|   // Paste with the code editor focused | ||||
|   // Following this guidance: https://github.com/microsoft/playwright/issues/8114 | ||||
|   await codeEditorText.focus() | ||||
|   await page.keyboard.press(`${metaModifier}+KeyV`) | ||||
|   await page.keyboard.press('ControlOrMeta+KeyV') | ||||
|   await expect( | ||||
|     await page.evaluate( | ||||
|       () => document.querySelector('.cm-content')?.textContent | ||||
| @ -380,9 +371,9 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => { | ||||
|   await test.step(`Type code with sketch hotkeys, shouldn't fire`, async () => { | ||||
|     // Since there's code now, we have to get to the end of the line | ||||
|     await page.locator('.cm-line').last().click() | ||||
|     await page.keyboard.down(CtrlKey) | ||||
|     await page.keyboard.down('ControlOrMeta') | ||||
|     await page.keyboard.press('ArrowRight') | ||||
|     await page.keyboard.up(CtrlKey) | ||||
|     await page.keyboard.up('ControlOrMeta') | ||||
|  | ||||
|     await page.keyboard.press('Enter') | ||||
|     await page.keyboard.type('//') | ||||
|  | ||||
							
								
								
									
										83
									
								
								electron-builder.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,83 @@ | ||||
| appId: dev.zoo.modeling-app | ||||
|  | ||||
| directories: | ||||
|   output: out | ||||
|   buildResources: assets | ||||
|  | ||||
| files: | ||||
|   - .vite/** | ||||
|  | ||||
| mac: | ||||
|   category: public.app-category.developer-tools | ||||
|   artifactName: "${productName}-${version}-${arch}-${os}.${ext}" | ||||
|   target: | ||||
|     - target: dmg | ||||
|       arch: | ||||
|         - x64 | ||||
|         - arm64 | ||||
|     - target: zip | ||||
|       arch: | ||||
|         - x64 | ||||
|         - arm64 | ||||
|   notarize: | ||||
|     teamId: 92H8YB3B95 | ||||
|   fileAssociations: | ||||
|     - ext: kcl | ||||
|       name: kcl | ||||
|       mimeType: text/vnd.zoo.kcl | ||||
|       description: Zoo KCL File | ||||
|       role: Editor | ||||
|       rank: Owner | ||||
|  | ||||
| win: | ||||
|   artifactName: "${productName}-${version}-${arch}-${os}.${ext}" | ||||
|   target: | ||||
|     - target: nsis | ||||
|       arch: | ||||
|         - x64 | ||||
|         - arm64 | ||||
|     - target: msi | ||||
|       arch: | ||||
|         - x64 | ||||
|         - arm64 | ||||
|   signingHashAlgorithms: | ||||
|     - sha256 | ||||
|   sign: "./sign-win.js" | ||||
|   publisherName: "KittyCAD Inc"  # needs to be exactly like on Digicert | ||||
|   icon: "assets/icon.ico" | ||||
|   fileAssociations: | ||||
|     - ext: kcl | ||||
|       name: kcl | ||||
|       mimeType: text/vnd.zoo.kcl | ||||
|       description: Zoo KCL File | ||||
|       role: Editor | ||||
|  | ||||
| msi: | ||||
|   oneClick: false | ||||
|   perMachine: true | ||||
|  | ||||
| nsis: | ||||
|   oneClick: false | ||||
|   perMachine: true | ||||
|   allowElevation: true | ||||
|   installerIcon: "assets/icon.ico" | ||||
|   include: "./installer.nsh" | ||||
|  | ||||
| linux: | ||||
|   artifactName: "${productName}-${version}-${arch}-${os}.${ext}" | ||||
|   target: | ||||
|     - target: appImage | ||||
|       arch: | ||||
|         - x64 | ||||
|         - arm64 | ||||
|   fileAssociations: | ||||
|     - ext: kcl | ||||
|       name: kcl | ||||
|       mimeType: text/vnd.zoo.kcl | ||||
|       description: Zoo KCL File | ||||
|       role: Editor | ||||
|  | ||||
| publish: | ||||
|   - provider: generic | ||||
|     url: https://dl.zoo.dev/releases/modeling-app | ||||
|     channel: latest | ||||
| @ -4,10 +4,17 @@ import { MakerZIP } from '@electron-forge/maker-zip' | ||||
| import { MakerDeb } from '@electron-forge/maker-deb' | ||||
| import { MakerRpm } from '@electron-forge/maker-rpm' | ||||
| import { VitePlugin } from '@electron-forge/plugin-vite' | ||||
| import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix' | ||||
| import { FusesPlugin } from '@electron-forge/plugin-fuses' | ||||
| import { FuseV1Options, FuseVersion } from '@electron/fuses' | ||||
| import path from 'path' | ||||
|  | ||||
| interface ExtendedMakerWixConfig extends MakerWixConfig { | ||||
|   // see https://github.com/electron/forge/issues/3673 | ||||
|   // this is an undocumented property of electron-wix-msi | ||||
|   associateExtensions?: string | ||||
| } | ||||
|  | ||||
| const rootDir = process.cwd() | ||||
|  | ||||
| const config: ForgeConfig = { | ||||
| @ -23,12 +30,23 @@ const config: ForgeConfig = { | ||||
|       undefined, | ||||
|     executableName: 'zoo-modeling-app', | ||||
|     icon: path.resolve(rootDir, 'assets', 'icon'), | ||||
|     protocols: [ | ||||
|       { | ||||
|         name: 'Zoo Studio', | ||||
|         schemes: ['zoo-studio'], | ||||
|       }, | ||||
|     ], | ||||
|     extendInfo: 'Info.plist', // Information for file associations. | ||||
|   }, | ||||
|   rebuildConfig: {}, | ||||
|   makers: [ | ||||
|     new MakerSquirrel({ | ||||
|       setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'), | ||||
|     }), | ||||
|     new MakerWix({ | ||||
|       icon: path.resolve(rootDir, 'assets', 'icon.ico'), | ||||
|       associateExtensions: 'kcl', | ||||
|     } as ExtendedMakerWixConfig), | ||||
|     new MakerZIP({}, ['darwin']), | ||||
|     new MakerRpm({ | ||||
|       options: { | ||||
|  | ||||
| @ -15,6 +15,7 @@ | ||||
|     /> | ||||
|     <link rel="apple-touch-icon" href="/logo192.png" /> | ||||
|     <link rel="manifest" href="/manifest.json" /> | ||||
|     <link rel="stylesheet" href="./inter/inter.css" /> | ||||
|     <link rel="stylesheet" href="https://use.typekit.net/zzv8rvm.css" /> | ||||
|     <script | ||||
|       defer | ||||
|  | ||||
							
								
								
									
										8
									
								
								installer.nsh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,8 @@ | ||||
| !macro preInit | ||||
| 	SetRegView 64 | ||||
| 	WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App" | ||||
| 	WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App" | ||||
| 	SetRegView 32 | ||||
| 	WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App" | ||||
| 	WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App" | ||||
| !macroend | ||||
							
								
								
									
										1
									
								
								interface.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -30,7 +30,6 @@ export interface IElectronAPI { | ||||
|   join: typeof path.join | ||||
|   sep: typeof path.sep | ||||
|   rename: (prev: string, next: string) => typeof fs.rename | ||||
|   setBaseUrl: (value: string) => void | ||||
|   packageJson: { | ||||
|     name: string | ||||
|   } | ||||
|  | ||||
							
								
								
									
										48
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "zoo-modeling-app", | ||||
|   "version": "0.24.12", | ||||
|   "version": "0.25.1", | ||||
|   "private": true, | ||||
|   "productName": "Zoo Modeling App", | ||||
|   "author": { | ||||
| @ -26,7 +26,7 @@ | ||||
|     "@fortawesome/react-fontawesome": "^0.2.0", | ||||
|     "@headlessui/react": "^1.7.19", | ||||
|     "@headlessui/tailwindcss": "^0.2.0", | ||||
|     "@kittycad/lib": "^2.0.0", | ||||
|     "@kittycad/lib": "^2.0.1", | ||||
|     "@lezer/highlight": "^1.2.1", | ||||
|     "@lezer/lr": "^1.4.1", | ||||
|     "@react-hook/resize-observer": "^2.0.1", | ||||
| @ -34,22 +34,24 @@ | ||||
|     "@ts-stack/markdown": "^1.5.0", | ||||
|     "@tweenjs/tween.js": "^23.1.1", | ||||
|     "@xstate/inspect": "^0.8.0", | ||||
|     "@xstate/react": "^3.2.2", | ||||
|     "@xstate/react": "^4.1.1", | ||||
|     "bonjour-service": "^1.2.1", | ||||
|     "codemirror": "^6.0.1", | ||||
|     "decamelize": "^6.0.0", | ||||
|     "electron-squirrel-startup": "^1.0.1", | ||||
|     "electron-updater": "^6.3.0", | ||||
|     "fuse.js": "^7.0.0", | ||||
|     "html2canvas-pro": "^1.5.8", | ||||
|     "isomorphic-fetch": "^3.0.0", | ||||
|     "json-rpc-2.0": "^1.6.0", | ||||
|     "jszip": "^3.10.1", | ||||
|     "minimist": "^1.2.8", | ||||
|     "openid-client": "^5.6.5", | ||||
|     "re-resizable": "^6.9.11", | ||||
|     "react": "^18.3.1", | ||||
|     "react-dom": "^18.2.0", | ||||
|     "react-hot-toast": "^2.4.1", | ||||
|     "react-hotkeys-hook": "^4.5.0", | ||||
|     "react-hotkeys-hook": "^4.5.1", | ||||
|     "react-json-view": "^1.21.3", | ||||
|     "react-modal": "^3.16.1", | ||||
|     "react-modal-promise": "^1.0.2", | ||||
| @ -62,7 +64,7 @@ | ||||
|     "vscode-languageserver-protocol": "^3.17.5", | ||||
|     "vscode-uri": "^3.0.8", | ||||
|     "web-vitals": "^3.5.2", | ||||
|     "xstate": "^4.38.2" | ||||
|     "xstate": "^5.17.4" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "start": "vite", | ||||
| @ -82,14 +84,13 @@ | ||||
|     "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages", | ||||
|     "fetch:wasm": "./get-latest-wasm-bundle.sh", | ||||
|     "isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)", | ||||
|     "build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt", | ||||
|     "build:wasm": "(cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt", | ||||
|     "build:wasm-clean": "yarn wasm-prep && yarn build:wasm", | ||||
|     "build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt", | ||||
|     "build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt", | ||||
|     "remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"", | ||||
|     "wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings", | ||||
|     "lint": "eslint --fix src e2e", | ||||
|     "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json", | ||||
|     "postinstall": "yarn xstate:typegen", | ||||
|     "wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings", | ||||
|     "lint": "eslint --fix src e2e packages/codemirror-lsp-client", | ||||
|     "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json", | ||||
|     "postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild", | ||||
|     "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"", | ||||
|     "make:dev": "make dev", | ||||
|     "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts", | ||||
| @ -97,7 +98,9 @@ | ||||
|     "tron:package": "electron-forge package", | ||||
|     "tron:make": "electron-forge make", | ||||
|     "tron:publish": "electron-forge publish", | ||||
|     "tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron" | ||||
|     "tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron", | ||||
|     "tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts", | ||||
|     "tronb:package": "electron-builder --config electron-builder.yml" | ||||
|   }, | ||||
|   "prettier": { | ||||
|     "trailingComma": "es5", | ||||
| @ -119,16 +122,18 @@ | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/plugin-proposal-private-property-in-object": "^7.21.11", | ||||
|     "@babel/preset-env": "^7.24.3", | ||||
|     "@babel/preset-env": "^7.25.4", | ||||
|     "@electron-forge/cli": "^7.4.0", | ||||
|     "@electron-forge/maker-deb": "^7.4.0", | ||||
|     "@electron-forge/maker-rpm": "^7.4.0", | ||||
|     "@electron-forge/maker-squirrel": "^7.4.0", | ||||
|     "@electron-forge/maker-wix": "^7.4.0", | ||||
|     "@electron-forge/maker-zip": "^7.4.0", | ||||
|     "@electron-forge/plugin-auto-unpack-natives": "^7.4.0", | ||||
|     "@electron-forge/plugin-fuses": "^7.4.0", | ||||
|     "@electron-forge/plugin-vite": "^7.4.0", | ||||
|     "@electron/fuses": "^1.8.0", | ||||
|     "@electron/rebuild": "^3.6.0", | ||||
|     "@iarna/toml": "^2.2.5", | ||||
|     "@lezer/generator": "^1.7.1", | ||||
|     "@playwright/test": "^1.46.1", | ||||
| @ -137,8 +142,9 @@ | ||||
|     "@types/d3-force": "^3.0.10", | ||||
|     "@types/electron": "^1.6.10", | ||||
|     "@types/isomorphic-fetch": "^0.0.39", | ||||
|     "@types/minimist": "^1.2.5", | ||||
|     "@types/mocha": "^10.0.6", | ||||
|     "@types/node": "^22.4.2", | ||||
|     "@types/node": "^22.5.0", | ||||
|     "@types/pixelmatch": "^5.2.6", | ||||
|     "@types/pngjs": "^6.0.4", | ||||
|     "@types/react": "^18.3.4", | ||||
| @ -147,7 +153,6 @@ | ||||
|     "@types/three": "^0.163.0", | ||||
|     "@types/ua-parser-js": "^0.7.39", | ||||
|     "@types/uuid": "^9.0.8", | ||||
|     "@types/wait-on": "^5.3.4", | ||||
|     "@types/wicg-file-system-access": "^2023.10.5", | ||||
|     "@types/ws": "^8.5.10", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.0.0", | ||||
| @ -158,31 +163,32 @@ | ||||
|     "autoprefixer": "^10.4.19", | ||||
|     "d3-force": "^3.0.0", | ||||
|     "electron": "^32.0.1", | ||||
|     "electron-builder": "^24.13.3", | ||||
|     "electron-notarize": "^1.2.2", | ||||
|     "eslint": "^8.0.1", | ||||
|     "eslint-config-react-app": "^7.0.1", | ||||
|     "eslint-plugin-css-modules": "^2.12.0", | ||||
|     "eslint-plugin-import": "^2.25.0", | ||||
|     "eslint-plugin-import": "^2.30.0", | ||||
|     "eslint-plugin-suggest-no-throw": "^1.0.0", | ||||
|     "happy-dom": "^14.3.10", | ||||
|     "http-server": "^14.1.1", | ||||
|     "husky": "^9.0.11", | ||||
|     "husky": "^9.1.5", | ||||
|     "node-fetch": "^3.3.2", | ||||
|     "pixelmatch": "^5.3.0", | ||||
|     "pngjs": "^7.0.0", | ||||
|     "postcss": "^8.4.31", | ||||
|     "postcss": "^8.4.43", | ||||
|     "postinstall-postinstall": "^2.1.0", | ||||
|     "prettier": "^2.8.8", | ||||
|     "setimmediate": "^1.0.5", | ||||
|     "tailwindcss": "^3.4.1", | ||||
|     "ts-node": "^10.0.0", | ||||
|     "typescript": "^5.0.0", | ||||
|     "vite": "^5.0.12", | ||||
|     "vite": "^5.4.2", | ||||
|     "vite-plugin-eslint": "^1.8.1", | ||||
|     "vite-plugin-package-version": "^1.1.0", | ||||
|     "vite-tsconfig-paths": "^4.3.2", | ||||
|     "vitest": "^1.6.0", | ||||
|     "vitest-webgl-canvas-mock": "^1.1.0", | ||||
|     "wait-on": "^7.2.0", | ||||
|     "wasm-pack": "^0.13.0", | ||||
|     "ws": "^8.17.0", | ||||
|     "yarn": "^1.22.22" | ||||
|  | ||||
| @ -72,6 +72,7 @@ export class LanguageServerClient { | ||||
|   async initialize() { | ||||
|     // Start the client in the background. | ||||
|     this.client.setNotifyFn(this.processNotifications.bind(this)) | ||||
|     // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||
|     this.client.start() | ||||
|  | ||||
|     this.ready = true | ||||
| @ -195,6 +196,9 @@ export class LanguageServerClient { | ||||
|   } | ||||
|  | ||||
|   private processNotifications(notification: LSP.NotificationMessage) { | ||||
|     for (const plugin of this.plugins) plugin.processNotification(notification) | ||||
|     for (const plugin of this.plugins) { | ||||
|       // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||
|       plugin.processNotification(notification) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -12,6 +12,7 @@ export default function lspFormatExt( | ||||
|       run: (view: EditorView) => { | ||||
|         let value = view.plugin(plugin) | ||||
|         if (!value) return false | ||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||
|         value.requestFormatting() | ||||
|         return true | ||||
|       }, | ||||
|  | ||||
| @ -117,6 +117,7 @@ export class LanguageServerPlugin implements PluginValue { | ||||
|  | ||||
|     this.processLspNotification = options.processLspNotification | ||||
|  | ||||
|     // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||
|     this.initialize({ | ||||
|       documentText: this.getDocText(), | ||||
|     }) | ||||
| @ -149,6 +150,7 @@ export class LanguageServerPlugin implements PluginValue { | ||||
|   } | ||||
|  | ||||
|   async initialize({ documentText }: { documentText: string }) { | ||||
|     // eslint-disable-next-line @typescript-eslint/no-misused-promises | ||||
|     if (this.client.initializePromise) { | ||||
|       await this.client.initializePromise | ||||
|     } | ||||
| @ -162,7 +164,9 @@ export class LanguageServerPlugin implements PluginValue { | ||||
|       }, | ||||
|     }) | ||||
|  | ||||
|     // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||
|     this.requestSemanticTokens() | ||||
|     // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||
|     this.updateFoldingRanges() | ||||
|   } | ||||
|  | ||||
| @ -225,7 +229,9 @@ export class LanguageServerPlugin implements PluginValue { | ||||
|         contentChanges: [{ text: this.view.state.doc.toString() }], | ||||
|       }) | ||||
|  | ||||
|       // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||
|       this.requestSemanticTokens() | ||||
|       // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||
|       this.updateFoldingRanges() | ||||
|     } catch (e) { | ||||
|       console.error(e) | ||||
| @ -526,7 +532,9 @@ export class LanguageServerPlugin implements PluginValue { | ||||
|   processDiagnostics(params: PublishDiagnosticsParams) { | ||||
|     if (params.uri !== this.getDocUri()) return | ||||
|  | ||||
|     const diagnostics = params.diagnostics | ||||
|     // Commented to avoid the lint.  See TODO below. | ||||
|     // const diagnostics = | ||||
|     params.diagnostics | ||||
|       .map(({ range, message, severity }) => ({ | ||||
|         from: posToOffset(this.view.state.doc, range.start)!, | ||||
|         to: posToOffset(this.view.state.doc, range.end)!, | ||||
|  | ||||
| @ -1,31 +0,0 @@ | ||||
| import { defineConfig, devices } from '@playwright/test' | ||||
| import dotenv from 'dotenv' | ||||
|  | ||||
| /** | ||||
|  * See https://playwright.dev/docs/test-configuration. | ||||
|  */ | ||||
| export default defineConfig({ | ||||
|   timeout: 120_000, // override the default 30s timeout | ||||
|   testDir: './e2e/playwright', | ||||
|   /* Run tests in files in parallel */ | ||||
|   fullyParallel: true, | ||||
|   /* Fail the build on CI if you accidentally left test.only in the source code. */ | ||||
|   forbidOnly: !!process.env.CI, | ||||
|   /* Do not retry */ | ||||
|   retries: process.env.CI ? 0 : 0, | ||||
|   /* Different amount of parallelism on CI and local. */ | ||||
|   workers: process.env.CI ? 1 : 4, | ||||
|   /* Reporter to use. See https://playwright.dev/docs/test-reporters */ | ||||
|   reporter: [ | ||||
|     [process.env.CI ? 'dot' : 'list'], | ||||
|     ['json', { outputFile: './test-results/report.json' }], | ||||
|     ['html'], | ||||
|   ], | ||||
|   /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ | ||||
|   use: { | ||||
|     /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ | ||||
|     trace: 'retain-on-failure', | ||||
|     actionTimeout: 15000, | ||||
|     screenshot: 'only-on-failure', | ||||
|   }, | ||||
| }) | ||||
| @ -17,6 +17,7 @@ export default defineConfig({ | ||||
|   /* Reporter to use. See https://playwright.dev/docs/test-reporters */ | ||||
|   reporter: [ | ||||
|     ['dot'], | ||||
|     ['list'], | ||||
|     ['json', { outputFile: './test-results/report.json' }], | ||||
|     ['html'], | ||||
|   ], | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { defineConfig } from '@playwright/test' | ||||
| import { defineConfig, devices } from '@playwright/test' | ||||
|  | ||||
| /** | ||||
|  * See https://playwright.dev/docs/test-configuration. | ||||
| @ -30,4 +30,24 @@ export default defineConfig({ | ||||
|     actionTimeout: 15_000, | ||||
|     screenshot: 'only-on-failure', | ||||
|   }, | ||||
|   projects: [ | ||||
|     { | ||||
|       name: 'Google Chrome', | ||||
|       use: { | ||||
|         ...devices['Desktop Chrome'], | ||||
|         channel: 'chrome', | ||||
|         contextOptions: { | ||||
|           /* Chromium is the only one with these permission types */ | ||||
|           permissions: ['clipboard-write', 'clipboard-read'], | ||||
|         }, | ||||
|         launchOptions: { | ||||
|           ...(process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH | ||||
|             ? { | ||||
|                 executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, | ||||
|               } | ||||
|             : {}), | ||||
|         }, | ||||
|       }, // or 'chrome-beta' | ||||
|     }, | ||||
|   ], | ||||
| }) | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								public/inter/InterVariable-Italic.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/inter/InterVariable.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										14
									
								
								public/inter/inter.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,14 @@ | ||||
| @font-face { | ||||
|   font-family: Inter; | ||||
|   font-style: normal; | ||||
|   font-weight: 100 900; | ||||
|   font-display: swap; | ||||
|   src: url("InterVariable.woff2") format("woff2"); | ||||
| } | ||||
| @font-face { | ||||
|   font-family: Inter; | ||||
|   font-style: italic; | ||||
|   font-weight: 100 900; | ||||
|   font-display: swap; | ||||
|   src: url("InterVariable-Italic.woff2") format("woff2"); | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								public/wheel-loop-dark.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								public/wheel-loop.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										38
									
								
								sign-win.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,38 @@ | ||||
| // From https://github.com/OpenBuilds/OpenBuilds-CONTROL/blob/4800540ffaa517925fc2cff26670809efa341ffe/signWin.js | ||||
| const { execSync } = require('node:child_process') | ||||
|  | ||||
| exports.default = async (configuration) => { | ||||
|   if (!process.env.SM_API_KEY) { | ||||
|     console.error( | ||||
|       'Signing using signWin.js script: failed: SM_API_KEY ENV VAR NOT FOUND' | ||||
|     ) | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   if (!process.env.WINDOWS_CERTIFICATE_THUMBPRINT) { | ||||
|     console.error( | ||||
|       'Signing using signWin.js script: failed: FINGERPRINT ENV VAR NOT FOUND' | ||||
|     ) | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   if (!configuration.path) { | ||||
|     throw new Error( | ||||
|       `Signing using signWin.js script: failed: TARGET PATH NOT FOUND` | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     execSync( | ||||
|       `smctl sign --fingerprint="${ | ||||
|         process.env.WINDOWS_CERTIFICATE_THUMBPRINT | ||||
|       }" --input "${String(configuration.path)}"`, | ||||
|       { | ||||
|         stdio: 'inherit', | ||||
|       } | ||||
|     ) | ||||
|     console.log('Signing using signWin.js script: successful') | ||||
|   } catch (error) { | ||||
|     console.error('Signing using signWin.js script: failed:', error) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										50
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						| @ -1,12 +1,8 @@ | ||||
| import { MouseEventHandler, useEffect, useMemo, useRef } from 'react' | ||||
| import { uuidv4 } from 'lib/utils' | ||||
| import { useEffect, useMemo, useRef } from 'react' | ||||
| import { useHotKeyListener } from './hooks/useHotKeyListener' | ||||
| import { Stream } from './components/Stream' | ||||
| import { EngineCommand } from 'lang/std/artifactGraph' | ||||
| import { throttle } from './lib/utils' | ||||
| import { AppHeader } from './components/AppHeader' | ||||
| import { useHotkeys } from 'react-hotkeys-hook' | ||||
| import { getNormalisedCoordinates } from './lib/utils' | ||||
| import { useLoaderData, useNavigate } from 'react-router-dom' | ||||
| import { type IndexLoaderData } from 'lib/types' | ||||
| import { PATHS } from 'lib/paths' | ||||
| @ -14,7 +10,6 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||
| import { onboardingPaths } from 'routes/Onboarding/paths' | ||||
| import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions' | ||||
| import { codeManager, engineCommandManager } from 'lib/singletons' | ||||
| import { useModelingContext } from 'hooks/useModelingContext' | ||||
| import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
| import { useLspContext } from 'components/LspProvider' | ||||
| @ -44,7 +39,6 @@ export function App() { | ||||
|   }, [projectName, projectPath]) | ||||
|  | ||||
|   useHotKeyListener() | ||||
|   const { context, state } = useModelingContext() | ||||
|  | ||||
|   const { auth, settings } = useSettingsAuthContext() | ||||
|   const token = auth?.context?.token | ||||
| @ -73,52 +67,14 @@ export function App() { | ||||
|     (p) => p === onboardingStatus.current | ||||
|   ) | ||||
|     ? 'opacity-20' | ||||
|     : context.store?.didDragInStream | ||||
|     ? 'opacity-40' | ||||
|     : '' | ||||
|  | ||||
|   useEngineConnectionSubscriptions() | ||||
|  | ||||
|   const debounceSocketSend = throttle<EngineCommand>((message) => { | ||||
|     engineCommandManager.sendSceneCommand(message) | ||||
|   }, 1000 / 15) | ||||
|   const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => { | ||||
|     if (state.matches('Sketch')) { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     const { x, y } = getNormalisedCoordinates({ | ||||
|       clientX: e.clientX, | ||||
|       clientY: e.clientY, | ||||
|       el: e.currentTarget, | ||||
|       ...context.store?.streamDimensions, | ||||
|     }) | ||||
|  | ||||
|     const newCmdId = uuidv4() | ||||
|     if (state.matches('idle.showPlanes')) return | ||||
|     if (context.store?.buttonDownInStream !== undefined) return | ||||
|     debounceSocketSend({ | ||||
|       type: 'modeling_cmd_req', | ||||
|       cmd: { | ||||
|         type: 'highlight_set_entity', | ||||
|         selected_at_window: { x, y }, | ||||
|       }, | ||||
|       cmd_id: newCmdId, | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
|       className="relative h-full flex flex-col" | ||||
|       onMouseMove={handleMouseMove} | ||||
|       ref={ref} | ||||
|     > | ||||
|     <div className="relative h-full flex flex-col" ref={ref}> | ||||
|       <AppHeader | ||||
|         className={ | ||||
|           'transition-opacity transition-duration-75 ' + | ||||
|           paneOpacity + | ||||
|           (context.store?.buttonDownInStream ? ' pointer-events-none' : '') | ||||
|         } | ||||
|         className={'transition-opacity transition-duration-75 ' + paneOpacity} | ||||
|         project={{ project, file }} | ||||
|         enableMenu={true} | ||||
|       /> | ||||
|  | ||||
| @ -33,7 +33,6 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider' | ||||
| import LspProvider from 'components/LspProvider' | ||||
| import { KclContextProvider } from 'lang/KclProvider' | ||||
| import { BROWSER_PROJECT_NAME } from 'lib/constants' | ||||
| import { getState, setState } from 'lib/desktop' | ||||
| import { CoreDumpManager } from 'lib/coredump' | ||||
| import { codeManager, engineCommandManager } from 'lib/singletons' | ||||
| import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||
| @ -42,6 +41,7 @@ import toast from 'react-hot-toast' | ||||
| import { coreDump } from 'lang/wasm' | ||||
| import { useMemo } from 'react' | ||||
| import { AppStateProvider } from 'AppState' | ||||
| import { reportRejection } from 'lib/trap' | ||||
|  | ||||
| const createRouter = isDesktop() ? createHashRouter : createBrowserRouter | ||||
|  | ||||
| @ -70,23 +70,6 @@ const router = createRouter([ | ||||
|         path: PATHS.INDEX, | ||||
|         loader: async () => { | ||||
|           const onDesktop = isDesktop() | ||||
|           if (onDesktop) { | ||||
|             const appState = await getState() | ||||
|  | ||||
|             if (appState) { | ||||
|               // Reset the state. | ||||
|               // We do this so that we load the initial state from the cli but everything | ||||
|               // else we can ignore. | ||||
|               await setState(undefined) | ||||
|               // Redirect to the file if we have a file path. | ||||
|               if (appState.current_file) { | ||||
|                 return redirect( | ||||
|                   PATHS.FILE + '/' + encodeURIComponent(appState.current_file) | ||||
|                 ) | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           return onDesktop | ||||
|             ? redirect(PATHS.HOME) | ||||
|             : redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME) | ||||
| @ -190,22 +173,24 @@ function CoreDump() { | ||||
|     () => new CoreDumpManager(engineCommandManager, codeManager, token), | ||||
|     [] | ||||
|   ) | ||||
|   useHotkeyWrapper(['meta + shift + .'], () => { | ||||
|     toast.promise( | ||||
|       coreDump(coreDumpManager, true), | ||||
|       { | ||||
|         loading: 'Starting core dump...', | ||||
|         success: 'Core dump completed successfully', | ||||
|         error: 'Error while exporting core dump', | ||||
|       }, | ||||
|       { | ||||
|         success: { | ||||
|           // Note: this extended duration is especially important for Playwright e2e testing | ||||
|           // default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations | ||||
|           duration: 6000, | ||||
|   useHotkeyWrapper(['mod + shift + .'], () => { | ||||
|     toast | ||||
|       .promise( | ||||
|         coreDump(coreDumpManager, true), | ||||
|         { | ||||
|           loading: 'Starting core dump...', | ||||
|           success: 'Core dump completed successfully', | ||||
|           error: 'Error while exporting core dump', | ||||
|         }, | ||||
|       } | ||||
|     ) | ||||
|         { | ||||
|           success: { | ||||
|             // Note: this extended duration is especially important for Playwright e2e testing | ||||
|             // default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations | ||||
|             duration: 6000, | ||||
|           }, | ||||
|         } | ||||
|       ) | ||||
|       .catch(reportRejection) | ||||
|   }) | ||||
|   return null | ||||
| } | ||||
|  | ||||