Compare commits

..

1 Commits

Author SHA1 Message Date
ab5b83bbce Increase test timeout 2024-08-28 12:52:53 +10:00
288 changed files with 11696 additions and 34158 deletions

View File

@ -2,9 +2,7 @@ NODE_ENV=development
DEV=true DEV=true
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev 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_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SKIP_AUTH=false VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000 VITE_KC_CONNECTION_TIMEOUT_MS=5000
# 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"
#VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"

View File

@ -13,8 +13,6 @@
"plugin:css-modules/recommended" "plugin:css-modules/recommended"
], ],
"rules": { "rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"semi": [ "semi": [
"error", "error",
"never" "never"
@ -26,6 +24,7 @@
{ {
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
"rules": { "rules": {
"@typescript-eslint/no-floating-promises": "warn",
"suggest-no-throw/suggest-no-throw": "off", "suggest-no-throw/suggest-no-throw": "off",
"testing-library/prefer-screen-queries": "off", "testing-library/prefer-screen-queries": "off",
"jest/valid-expect": "off" "jest/valid-expect": "off"

View File

@ -1,4 +1,4 @@
name: build-publish-apps name: build-test-publish-apps
on: on:
pull_request: pull_request:
@ -21,7 +21,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
prepare-files: prepare-json-files:
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows) runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs: outputs:
version: ${{ steps.export_version.outputs.version }} version: ${{ steps.export_version.outputs.version }}
@ -33,80 +33,45 @@ jobs:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' 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 - name: Set nightly version
if: github.event_name == 'schedule' if: github.event_name == 'schedule'
run: | run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
# TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json # TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json
# 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 - uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
with: with:
name: prepared-files
path: | path: |
package.json package.json
src/wasm-lib/pkg/wasm_lib*
- id: export_version - id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT" run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test"' electron-builder.yml
- uses: actions/upload-artifact@v3 build-test-app-macos:
with: needs: [prepare-json-files]
name: prepared-files-updater-test runs-on: macos-14
path: |
electron-builder.yml
build-apps:
needs: [prepare-files]
strategy:
fail-fast: false
matrix:
os: [macos-14, windows-2022, ubuntu-22.04]
runs-on: ${{ matrix.os }}
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_SIGNING_IDENTITY: ${{ 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
name: prepared-files if: github.event_name == 'schedule'
- name: Copy prepared files - name: Copy updated .json files
if: github.event_name == 'schedule'
run: | run: |
ls -R prepared-files ls -l artifact
cp prepared-files/package.json package.json cp artifact/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 - name: Sync node version and setup cache
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -116,10 +81,79 @@ jobs:
- run: yarn install - run: yarn install
- run: yarn tronb:vite - 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
- name: Prepare certificate and variables (Windows only) - name: Prepare certificate and variables (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }} if: ${{ env.BUILD_RELEASE == 'true' }}
run: | run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12 echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12 cat /d/Certificate_pkcs12.p12
@ -134,7 +168,7 @@ jobs:
shell: bash shell: bash
- name: Setup certicate with SSM KSP (Windows only) - name: Setup certicate with SSM KSP (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }} if: ${{ env.BUILD_RELEASE == 'true' }}
run: | 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 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 msiexec /i smtools-windows-x64.msi /quiet /qn
@ -144,42 +178,83 @@ jobs:
smksp_cert_sync.exe smksp_cert_sync.exe
shell: cmd shell: cmd
- name: Build the app - name: Build the app for x64
run: yarn electron-builder --config ${{ env.BUILD_RELEASE && '--publish always' || '' }} run: "yarn electron-forge make --arch x64"
- name: List artifacts in out/ - name: Build the app for arm64
run: ls -R out 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 }}"
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: out-${{ matrix.os }} path: "out/make/*/*/*"
path: |
out/Zoo*.* # TODO: Run e2e tests
out/latest*.yml
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"
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back # TODO: add the 'Build for Mac TestFlight (nightly)' stage back
- uses: actions/download-artifact@v3 # TODO: sign the app (and updater bundle potentially)
if: ${{ env.CUT_RELEASE_PR == 'true' }}
name: prepared-files-updater-test
- name: Copy updated electron-builder.yml file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
ls -R prepared-files-updater-test
cp prepared-files-updater-test/electron-builder.yml electron-builder.yml
- name: Build the app (updater-test)
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: yarn electron-builder --config ${{ env.BUILD_RELEASE && '--publish always' || '' }}
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with: with:
name: updater-test-${{ matrix.os }} path: "out/make/*/*/*"
path: |
out/Zoo*.*
out/latest*.yml
publish-apps-release: publish-apps-release:
@ -187,76 +262,88 @@ jobs:
permissions: permissions:
contents: write contents: write
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }} if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [prepare-files, build-apps] needs: [prepare-json-files, build-test-app-macos, build-test-app-windows, build-test-app-ubuntu]
env: env:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }} VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }} 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('Non-release build, commit {0}', github.sha) }} NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }} BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }} WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }} URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
steps: steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with:
name: out-windows-2022
path: out
- uses: actions/download-artifact@v3 - name: Generate the update static endpoint
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: | run: |
RELEASE_DIR=https://${WEBSITE_DIR} 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 \ jq --null-input \
--arg version "${VERSION}" \ --arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \ --arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \ --arg notes "${NOTES}" \
--arg mac_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-mac.dmg" \ --arg darwin_sig "$DARWIN_SIG" \
--arg mac_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-mac.dmg" \ --arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \
--arg windows_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-win.exe" \ --arg windows_x86_64_sig "$WINDOWS_X86_64_SIG" \
--arg windows_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-win.exe" \ --arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \
--arg linux_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-linux.AppImage" \ --arg windows_aarch64_sig "$WINDOWS_AARCH64_SIG" \
--arg linux_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x86_64-linux.AppImage" \ --arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi.zip" \
'{ '{
"version": $version, "version": $version,
"pub_date": $pub_date, "pub_date": $pub_date,
"notes": $notes, "notes": $notes,
"platforms": { "platforms": {
"dmg-arm64": { "darwin-x86_64": {
"url": $mac_arm64_url "signature": $darwin_sig,
"url": $darwin_url
}, },
"dmg-x64": { "darwin-aarch64": {
"url": $mac_x64_url "signature": $darwin_sig,
"url": $darwin_url
}, },
"exe-arm64": { "windows-x86_64": {
"url": $windows_arm64_url "signature": $windows_x86_64_sig,
"url": $windows_x86_64_url
}, },
"exe-x64": { "windows-aarch64": {
"url": $windows_x64_url "signature": $windows_aarch64_sig,
"url": $windows_aarch64_url
}
}
}' > last_update.json
cat last_update.json
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
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" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"dmg-universal": {
"url": $darwin_url
}, },
"appimage-arm64": { "msi-x86_64": {
"url": $linux_arm64_url "url": $windows_x86_64_url
}, },
"appimage-x64": { "msi-aarch64": {
"url": $linux_x64_url "url": $windows_aarch64_url
} }
} }
}' > last_download.json }' > last_download.json
cat last_download.json cat last_download.json
- name: List artifacts
run: "ls -R out"
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.5' uses: 'google-github-actions/auth@v2.1.5'
with: with:
@ -265,26 +352,24 @@ jobs:
- name: Set up Google Cloud SDK - name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v2.1.0 uses: google-github-actions/setup-gcloud@v2.1.0
with: with:
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }} project_id: kittycadapi
- name: Upload release files to public bucket - name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0 uses: google-github-actions/upload-cloud-storage@v2.1.3
with: with:
path: out path: artifact
glob: 'Zoo*' glob: '*/Zoo*'
parent: false parent: false
destination: ${{ env.BUCKET_DIR }} destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket - name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0 uses: google-github-actions/upload-cloud-storage@v2.1.3
with: with:
path: out path: last_update.json
glob: 'latest*'
parent: false
destination: ${{ env.BUCKET_DIR }} destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket - name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0 uses: google-github-actions/upload-cloud-storage@v2.1.3
with: with:
path: last_download.json path: last_download.json
destination: ${{ env.BUCKET_DIR }} destination: ${{ env.BUCKET_DIR }}
@ -293,9 +378,7 @@ jobs:
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: 'out/Zoo*' files: 'artifact/*/Zoo*'
# TODO: Add GitHub publisher
announce_release: announce_release:
needs: [publish-apps-release] needs: [publish-apps-release]

View File

@ -45,7 +45,7 @@ jobs:
- run: yarn xstate:typegen - run: yarn xstate:typegen
- run: yarn tsc - run: yarn tsc
- name: Lint - name: Lint
run: yarn eslint --max-warnings 0 src e2e packages/codemirror-lsp-client run: yarn eslint --max-warnings 0 src e2e
check-typos: check-typos:

View File

@ -28,7 +28,6 @@ jobs:
dir: ['src/wasm-lib'] dir: ['src/wasm-lib']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: taiki-e/install-action@just
- name: Install latest rust - name: Install latest rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
@ -42,7 +41,7 @@ jobs:
- name: Run clippy - name: Run clippy
run: | run: |
cd "${{ matrix.dir }}" cd "${{ matrix.dir }}"
just lint cargo clippy --all --tests --benches -- -D warnings
# If this fails, run "cargo check" to update Cargo.lock, # If this fails, run "cargo check" to update Cargo.lock,
# then add Cargo.lock to the PR. # then add Cargo.lock to the PR.
- name: Check Cargo.lock doesn't need updating - name: Check Cargo.lock doesn't need updating

View File

@ -34,7 +34,7 @@ jobs:
- 'src/wasm-lib/**' - 'src/wasm-lib/**'
playwright-chrome: playwright-chrome:
timeout-minutes: ${{ matrix.os == 'macos-14' && 60 || 50 }} timeout-minutes: ${{ matrix.os == 'macos-14' && 60 || 40 }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -139,7 +139,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
with: with:
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }} name: playwright-report-ubuntu-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
@ -174,14 +174,14 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: steps.git-check.outputs.modified == 'true' if: steps.git-check.outputs.modified == 'true'
with: with:
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true continue-on-error: true
with: with:
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run playwright/chrome flow (with retries) - name: Run playwright/chrome flow (with retries)
id: retry id: retry
@ -232,7 +232,6 @@ jobs:
exit 0 exit 0
env: env:
CI: true CI: true
FAIL_ON_CONSOLE_ERRORS: true
NODE_ENV: development NODE_ENV: development
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true VITE_KC_SKIP_AUTH: true
@ -245,14 +244,14 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/ path: test-results/
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
@ -263,7 +262,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-14] os: [ubuntu-latest, windows-latest, macos-14]
timeout-minutes: 40 timeout-minutes: 30
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: check-rust-changes needs: check-rust-changes
steps: steps:
@ -352,7 +351,7 @@ jobs:
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true continue-on-error: true
with: with:
name: test-results-${{ matrix.os }}-${{ github.sha }} name: test-results-ubuntu-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run electron tests (with retries) - name: Run electron tests (with retries)
id: retry id: retry
@ -382,7 +381,7 @@ jobs:
echo "retried=true" >>$GITHUB_OUTPUT echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry" echo "run playwright with last failed tests and retry $retry"
if [[ "$IS_UBUNTU" == "true" ]]; then if [[ "$IS_UBUNTU" == "true" ]]; then
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --last-failed --grep=@electron || true xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
else else
yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
fi fi
@ -411,7 +410,6 @@ jobs:
exit 0 exit 0
env: env:
CI: true CI: true
FAIL_ON_CONSOLE_ERRORS: true
NODE_ENV: development NODE_ENV: development
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true VITE_KC_SKIP_AUTH: true

1
.gitignore vendored
View File

@ -59,7 +59,6 @@ src/wasm-lib/grackle/stdlib_cube_partial.json
Mac_App_Distribution.provisionprofile Mac_App_Distribution.provisionprofile
*.tsbuildinfo *.tsbuildinfo
src/wasm-lib/pkg
venv venv
.vite/ .vite/

View File

@ -7,14 +7,6 @@ XSTATE_TYPEGENS := $(wildcard src/machines/*.typegen.ts)
dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS) dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS)
yarn start 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) $(XSTATE_TYPEGENS): $(TS_SRC)
yarn xstate typegen 'src/**/*.ts?(x)' yarn xstate typegen 'src/**/*.ts?(x)'

View File

@ -351,6 +351,25 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
</details> </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 ## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl). For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).

24
add-osx-cert.sh Normal file
View File

@ -0,0 +1,24 @@
#!/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

View File

@ -22,3 +22,8 @@ once fixed in engine will just start working here with no language changes.
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple - **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
chamfer cases work currently. 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.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,6 @@ layout: manual
* [`angledLineToX`](kcl/angledLineToX) * [`angledLineToX`](kcl/angledLineToX)
* [`angledLineToY`](kcl/angledLineToY) * [`angledLineToY`](kcl/angledLineToY)
* [`arc`](kcl/arc) * [`arc`](kcl/arc)
* [`arrayReduce`](kcl/arrayReduce)
* [`asin`](kcl/asin) * [`asin`](kcl/asin)
* [`assert`](kcl/assert) * [`assert`](kcl/assert)
* [`assertEqual`](kcl/assertEqual) * [`assertEqual`](kcl/assertEqual)
@ -57,7 +56,6 @@ layout: manual
* [`line`](kcl/line) * [`line`](kcl/line)
* [`lineTo`](kcl/lineTo) * [`lineTo`](kcl/lineTo)
* [`ln`](kcl/ln) * [`ln`](kcl/ln)
* [`loft`](kcl/loft)
* [`log`](kcl/log) * [`log`](kcl/log)
* [`log10`](kcl/log10) * [`log10`](kcl/log10)
* [`log2`](kcl/log2) * [`log2`](kcl/log2)
@ -65,7 +63,6 @@ layout: manual
* [`max`](kcl/max) * [`max`](kcl/max)
* [`min`](kcl/min) * [`min`](kcl/min)
* [`mm`](kcl/mm) * [`mm`](kcl/mm)
* [`offsetPlane`](kcl/offsetPlane)
* [`patternCircular2d`](kcl/patternCircular2d) * [`patternCircular2d`](kcl/patternCircular2d)
* [`patternCircular3d`](kcl/patternCircular3d) * [`patternCircular3d`](kcl/patternCircular3d)
* [`patternLinear2d`](kcl/patternLinear2d) * [`patternLinear2d`](kcl/patternLinear2d)
@ -91,7 +88,6 @@ layout: manual
* [`tan`](kcl/tan) * [`tan`](kcl/tan)
* [`tangentialArc`](kcl/tangentialArc) * [`tangentialArc`](kcl/tangentialArc)
* [`tangentialArcTo`](kcl/tangentialArcTo) * [`tangentialArcTo`](kcl/tangentialArcTo)
* [`tangentialArcToRelative`](kcl/tangentialArcToRelative)
* [`tau`](kcl/tau) * [`tau`](kcl/tau)
* [`toDegrees`](kcl/toDegrees) * [`toDegrees`](kcl/toDegrees)
* [`toRadians`](kcl/toRadians) * [`toRadians`](kcl/toRadians)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,8 @@ const example = extrude(10, exampleSketch)
offset: number, offset: number,
// Radius of the arc. Not to be confused with Raiders of the Lost Ark. // Radius of the arc. Not to be confused with Raiders of the Lost Ark.
radius: number, radius: number,
} } |
[number, number]
``` ```
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. (REQUIRED) * `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. (REQUIRED)
```js ```js

File diff suppressed because one or more lines are too long

View File

@ -8,8 +8,8 @@ import {
PERSIST_MODELING_CONTEXT, PERSIST_MODELING_CONTEXT,
} from './test-utils' } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {
@ -96,49 +96,33 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
} }
// deselect line tool // deselect line tool
const btnLine = page.getByTestId('line') await page.getByTestId('line').click()
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) const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect await expect
.poll(async () => u.getGreatestPixDiff(line1, TEST_COLORS.WHITE)) .poll(async () => u.getGreatestPixDiff(line1, TEST_COLORS.WHITE))
.toBeLessThan(3) .toBeLessThan(3)
await page.waitForTimeout(100)
await expect await expect
.poll(async () => u.getGreatestPixDiff(line1, [249, 249, 249])) .poll(() => u.getGreatestPixDiff(line1, [249, 249, 249]))
.toBeLessThan(3) .toBeLessThan(3)
await page.waitForTimeout(100)
} }
// click between first two clicks to get center of the line // click between first two clicks to get center of the line
await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect( expect(await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)).toBeLessThan(3)
await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)
).toBeLessThan(3)
await expect(await u.getGreatestPixDiff(line1, [0, 0, 255])).toBeLessThan(3) await expect(await u.getGreatestPixDiff(line1, [0, 0, 255])).toBeLessThan(3)
} }
// hold down shift // hold down shift
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.waitForTimeout(100)
// click between the latest two clicks to get center of the line // click between the latest two clicks to get center of the line
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20)
await page.waitForTimeout(100)
// selected two lines therefore there should be two cursors // selected two lines therefore there should be two cursors
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(page.locator('.cm-cursor')).toHaveCount(2) await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.waitForTimeout(100)
} }
await page.getByRole('button', { name: 'Length: open menu' }).click() await page.getByRole('button', { name: 'Length: open menu' }).click()

View File

@ -3,8 +3,8 @@ import { getUtils, setup, tearDown } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {

View File

@ -12,8 +12,8 @@ import { bracket } from 'lib/exampleKcl'
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates' import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates'
import fsp from 'fs/promises' import fsp from 'fs/promises'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {
@ -27,19 +27,9 @@ test.describe('Code pane and errors', () => {
const u = await getUtils(page) const u = await getUtils(page)
// Load the app with the working starter code // Load the app with the working starter code
await page.addInitScript(() => { await page.addInitScript((code) => {
localStorage.setItem( localStorage.setItem('persistCode', code)
'persistCode', }, bracket)
`// 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 page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
@ -65,8 +55,6 @@ const extrude001 = extrude(5, sketch001)`
test('Opening and closing the code pane will consistently show error diagnostics', async ({ test('Opening and closing the code pane will consistently show error diagnostics', async ({
page, page,
}) => { }) => {
await page.goto('http://localhost:3000')
const u = await getUtils(page) const u = await getUtils(page)
// Load the app with the working starter code // Load the app with the working starter code
@ -92,7 +80,7 @@ const extrude001 = extrude(5, sketch001)`
// Delete a character to break the KCL // Delete a character to break the KCL
await u.openKclCodePanel() await u.openKclCodePanel()
await page.getByText('thickness, bracketLeg1Sketch)').click() await page.getByText('extrude(').click()
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
// Ensure that a badge appears on the button // Ensure that a badge appears on the button
@ -103,7 +91,7 @@ const extrude001 = extrude(5, sketch001)`
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
await expect(page.locator('.cm-tooltip').first()).toBeVisible() await expect(page.getByText('Unexpected token: |').first()).toBeVisible()
// Close the code pane // Close the code pane
await codePaneButton.click() await codePaneButton.click()
@ -126,7 +114,7 @@ const extrude001 = extrude(5, sketch001)`
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
await expect(page.locator('.cm-tooltip').first()).toBeVisible() await expect(page.getByText('Unexpected token: |').first()).toBeVisible()
}) })
test('When error is not in view you can click the badge to scroll to it', async ({ test('When error is not in view you can click the badge to scroll to it', async ({
@ -273,7 +261,10 @@ test(
await page.getByText('bracket').click() await page.getByText('bracket').click()
await u.waitForPageLoad() await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
}) })
// If they're open by default, we're not actually testing anything. // If they're open by default, we're not actually testing anything.
@ -301,7 +292,16 @@ test(
await page.getByText('router-template-slate').click() await page.getByText('router-template-slate').click()
await u.waitForPageLoad() 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 test.step('All panes opened before should be visible', async () => { await test.step('All panes opened before should be visible', async () => {

View File

@ -3,8 +3,8 @@ import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils' import { getUtils, setup, tearDown } from './test-utils'
import { KCL_DEFAULT_LENGTH } from 'lib/constants' import { KCL_DEFAULT_LENGTH } from 'lib/constants'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {

View File

@ -1,8 +1,8 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils' import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {

View File

@ -43,6 +43,12 @@ test(
// open the project // open the project
await page.getByText(`bracket`).click() 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 // expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -50,12 +56,6 @@ test(
const exportButton = page.getByTestId('export-pane-button') const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible() 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 gltfOption = page.getByText('glTF')
const submitButton = page.getByText('Confirm Export') const submitButton = page.getByText('Confirm Export')
const exportingToastMessage = page.getByText(`Exporting...`) const exportingToastMessage = page.getByText(`Exporting...`)
@ -104,7 +104,7 @@ test(
}, },
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBe(477481) .toBe(477327)
// clean up output.gltf // clean up output.gltf
await fsp.rm('output.gltf') await fsp.rm('output.gltf')

View File

@ -2,8 +2,8 @@ import { test, expect } from '@playwright/test'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { getUtils, setup, tearDown } from './test-utils' import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {

View File

@ -1,279 +0,0 @@
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 }, testInfo) => {
await setup(context, page, testInfo)
})
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,
})
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 page.waitForTimeout(500)
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()
})
}
)
})

View File

@ -1,270 +0,0 @@
export const isErrorWhitelisted = (exception: Error) => {
// due to the way webkit/Google Chrome report errors, it was necessary
// to whitelist similar errors separately for each project
let whitelist: {
name: string
message: string
stack: string
foundInSpec: string
project: 'webkit' | 'Google Chrome'
}[] = [
{
name: '',
message: 'undefined',
stack: '',
foundInSpec: `e2e/playwright/sketch-tests.spec.ts Existing sketch with bad code delete user's code`,
project: 'Google Chrome',
},
{
name: '"{"kind"',
message:
'"engine","sourceRanges":[[0,0]],"msg":"Failed to get string from response from engine: `JsValue(undefined)`"}"',
stack: '',
foundInSpec: 'e2e/playwright/testing-settings.spec.ts',
project: 'Google Chrome',
},
{
name: '',
message: 'false',
stack: '',
foundInSpec: 'e2e/playwright/testing-segment-overlays.spec.ts',
project: 'Google Chrome',
},
{
name: '{"kind"',
// eslint-disable-next-line no-useless-escape
message: 'no connection to send on',
stack: '',
foundInSpec: 'e2e/playwright/various.spec.ts',
project: 'Google Chrome',
},
{
name: '',
message: 'sketchGroup not found',
stack: '',
foundInSpec:
'e2e/playwright/testing-selections.spec.ts Deselecting line tool should mean nothing happens on click',
project: 'Google Chrome',
},
{
name: 'engine error',
message:
'[{"error_code":"bad_request","message":"Cannot set the camera position with these values"}]',
stack: '',
foundInSpec:
'e2e/playwright/can-create-sketches-on-all-planes-and-their-back-sides.spec.ts XY',
project: 'Google Chrome',
},
{
name: '',
message: 'no connection to send on',
stack: '',
foundInSpec:
'e2e/playwright/can-create-sketches-on-all-planes-and-their-back-sides.spec.ts XY',
project: 'Google Chrome',
},
{
name: 'RangeError',
message: 'Position 160 is out of range for changeset of length 0',
stack: `RangeError: Position 160 is out of range for changeset of length 0
at _ChangeSet.mapPos (http://localhost:3000/node_modules/.vite/deps/chunk-3BHLKIA4.js?v=412eae63:756:13)
at findSharedChunks (http://localhost:3000/node_modules/.vite/deps/chunk-3BHLKIA4.js?v=412eae63:3045:49)
at _RangeSet.compare (http://localhost:3000/node_modules/.vite/deps/chunk-3BHLKIA4.js?v=412eae63:2840:24)
at findChangedDeco (http://localhost:3000/node_modules/.vite/deps/chunk-IZYF444B.js?v=412eae63:3320:12)
at DocView.update (http://localhost:3000/node_modules/.vite/deps/chunk-IZYF444B.js?v=412eae63:2774:20)
at _EditorView.update (http://localhost:3000/node_modules/.vite/deps/chunk-IZYF444B.js?v=412eae63:7056:30)
at DOMObserver.flush (http://localhost:3000/node_modules/.vite/deps/chunk-IZYF444B.js?v=412eae63:6621:17)
at MutationObserver.<anonymous> (http://localhost:3000/node_modules/.vite/deps/chunk-IZYF444B.js?v=412eae63:6322:14)`,
foundInSpec: 'e2e/playwright/editor-tests.spec.ts fold gutters work',
project: 'Google Chrome',
},
{
name: 'RangeError',
message: 'Selection points outside of document',
stack: `RangeError: Selection points outside of document
+ at checkSelection (http://localhost:3000/node_modules/.vite/deps/chunk-3BHLKIA4.js?v=412eae63:1453:13)
+ at new _Transaction (http://localhost:3000/node_modules/.vite/deps/chunk-3BHLKIA4.js?v=412eae63:2014:7)
+ at _Transaction.create (http://localhost:3000/node_modules/.vite/deps/chunk-3BHLKIA4.js?v=412eae63:2022:12)
+ at resolveTransaction (http://localhost:3000/node_modules/.vite/deps/chunk-3BHLKIA4.js?v=412eae63:2155:24)
+ at _EditorState.update (http://localhost:3000/node_modules/.vite/deps/chunk-3BHLKIA4.js?v=412eae63:2281:12)
+ at _EditorView.dispatch (http://localhost:3000/node_modules/.vite/deps/chunk-IZYF444B.js?v=412eae63:6988:148)
+ at EditorManager.selectRange (http://localhost:3000/src/editor/manager.ts:182:22)
+ at AST extrude (http://localhost:3000/src/machines/modelingMachine.ts:828:25)`,
foundInSpec: 'e2e/playwright/editor-tests.spec.ts',
project: 'Google Chrome',
},
{
name: 'Unhandled Promise Rejection',
message: "TypeError: null is not an object (evaluating 'sg.value')",
stack: `Unhandled Promise Rejection: TypeError: null is not an object (evaluating 'sg.value')
at unknown (http://localhost:3000/src/clientSideScene/sceneEntities.ts:466:23)
at unknown (http://localhost:3000/src/clientSideScene/sceneEntities.ts:454:32)
at set up draft line without teardown (http://localhost:3000/src/machines/modelingMachine.ts:983:47)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=0de2e74f:1877:24)
at handleAction (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=0de2e74f:1064:26)
at processBlock (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=0de2e74f:1087:36)
at map ([native code]:0:0)
at resolveActions (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=0de2e74f:1109:49)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=0de2e74f:3639:37)
at provide (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=0de2e74f:1117:18)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=0de2e74f:2452:30)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=0de2e74f:1831:43)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=0de2e74f:1659:17)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=0de2e74f:1643:19)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=0de2e74f:1829:33)
at unknown (http://localhost:3000/src/clientSideScene/sceneEntities.ts:263:19)`,
foundInSpec: `e2e/playwright/testing-camera-movement.spec.ts Zoom should be consistent when exiting or entering sketches`,
project: 'webkit',
},
{
name: 'Unhandled Promise Rejection',
message: 'false',
stack: `Unhandled Promise Rejection: false
at unknown (http://localhost:3000/src/clientSideScene/ClientSideSceneComp.tsx:455:78)`,
foundInSpec: `e2e/playwright/testing-segment-overlays.spec.ts line-[tagOutsideSketch]`,
project: 'webkit',
},
{
name: 'Unhandled Promise Rejection',
message: `TypeError: null is not an object (evaluating 'programMemory.get(variableDeclarationName).value')`,
stack: ` + stack:Unhandled Promise Rejection: TypeError: null is not an object (evaluating 'programMemory.get(variableDeclarationName).value')
+ at unknown (http://localhost:3000/src/machines/modelingMachine.ts:911:49)`,
foundInSpec: `e2e/playwright/can-create-sketches-on-all-planes-and-their-back-sides.spec.ts`,
project: 'webkit',
},
{
name: 'Unhandled Promise Rejection',
message: `null is not an object (evaluating 'programMemory.get(variableDeclarationName).value')`,
stack: `Unhandled Promise Rejection: TypeError: null is not an object (evaluating 'programMemory.get(variableDeclarationName).value')
at unknown (http://localhost:3000/src/machines/modelingMachine.ts:911:49)`,
foundInSpec: `e2e/playwright/testing-camera-movement.spec.ts Zoom should be consistent when exiting or entering sketches`,
project: 'webkit',
},
{
name: 'TypeError',
message: `null is not an object (evaluating 'gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT).precision')`,
stack: `TypeError: null is not an object (evaluating 'gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT).precision')
at getMaxPrecision (http://localhost:3000/node_modules/.vite/deps/chunk-DEEFU7IG.js?v=d328572b:9557:71)
at WebGLCapabilities (http://localhost:3000/node_modules/.vite/deps/chunk-DEEFU7IG.js?v=d328572b:9570:39)
at initGLContext (http://localhost:3000/node_modules/.vite/deps/chunk-DEEFU7IG.js?v=d328572b:16993:43)
at WebGLRenderer (http://localhost:3000/node_modules/.vite/deps/chunk-DEEFU7IG.js?v=d328572b:17024:18)
at SceneInfra (http://localhost:3000/src/clientSideScene/sceneInfra.ts:185:38)
at module code (http://localhost:3000/src/lib/singletons.ts:14:41)`,
foundInSpec: `e2e/playwright/testing-segment-overlays.spec.ts angledLineToX`,
project: 'webkit',
},
{
name: 'Unhandled Promise Rejection',
message:
'{"kind":"engine","sourceRanges":[[0,0]],"msg":"Failed to get string from response from engine: `JsValue(undefined)`"}',
stack: `Unhandled Promise Rejection: {"kind":"engine","sourceRanges":[[0,0]],"msg":"Failed to get string from response from engine: \`JsValue(undefined)\`"}
at unknown (http://localhost:3000/src/lang/std/engineConnection.ts:1245:26)`,
foundInSpec:
'e2e/playwright/onboarding-tests.spec.ts Click through each onboarding step',
project: 'webkit',
},
{
name: 'Unhandled Promise Rejection',
message: 'undefined',
stack: '',
foundInSpec: `e2e/playwright/sketch-tests.spec.ts Existing sketch with bad code delete user's code`,
project: 'webkit',
},
{
name: 'Fetch API cannot load https',
message: '/api.dev.zoo.dev/logout due to access control checks.',
stack: `Fetch API cannot load https://api.dev.zoo.dev/logout due to access control checks.
at goToSignInPage (http://localhost:3000/src/components/SettingsAuthProvider.tsx:229:15)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=d328572b:1877:24)
at handleAction (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=d328572b:1064:26)
at processBlock (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=d328572b:1087:36)
at map (:1:11)
at resolveActions (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=d328572b:1109:49)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=d328572b:3639:37)
at provide (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=d328572b:1117:18)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=d328572b:2452:30)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=d328572b:1831:43)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=d328572b:1659:17)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=d328572b:1643:19)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=d328572b:1829:33)
at unknown (http://localhost:3000/node_modules/.vite/deps/chunk-6FRHHHSJ.js?v=d328572b:2601:23)`,
foundInSpec:
'e2e/playwright/testing-selections.spec.ts Solids should be select and deletable',
project: 'webkit',
},
{
name: 'Unhandled Promise Rejection',
message: 'ReferenceError: Cannot access uninitialized variable.',
stack: `Unhandled Promise Rejection: ReferenceError: Cannot access uninitialized variable.
at setDiagnosticsForCurrentErrors (http://localhost:3000/src/lang/KclSingleton.ts:90:18)
at kclErrors (http://localhost:3000/src/lang/KclSingleton.ts:82:40)
at safeParse (http://localhost:3000/src/lang/KclSingleton.ts:150:9)
at unknown (http://localhost:3000/src/lang/KclSingleton.ts:113:32)`,
foundInSpec:
'e2e/playwright/testing-segment-overlays.spec.ts angledLineToX',
project: 'webkit',
},
{
name: 'Unhandled Promise Rejection',
message: 'sketchGroup not found',
stack: `Unhandled Promise Rejection: sketchGroup not found
at unknown (http://localhost:3000/src/machines/modelingMachine.ts:911:49)`,
foundInSpec:
'e2e/playwright/testing-selections.spec.ts Deselecting line tool should mean nothing happens on click',
project: 'webkit',
},
{
name: 'Unhandled Promise Rejection',
message:
'engine error: [{"error_code":"bad_request","message":"Cannot set the camera position with these values"}]',
stack:
'Unhandled Promise Rejection: engine error: [{"error_code":"bad_request","message":"Cannot set the camera position with these values"}]',
foundInSpec:
'e2e/playwright/testing-camera-movement.spec.ts Zoom should be consistent when exiting or entering sketches',
project: 'webkit',
},
{
name: 'SecurityError',
stack: `SecurityError: Failed to read the 'localStorage' property from 'Window': Access is denied for this document.
at <anonymous>:13:5
at <anonymous>:18:5
at <anonymous>:19:7`,
message: `Failed to read the 'localStorage' property from 'Window': Access is denied for this document.`,
project: 'Google Chrome',
foundInSpec: 'e2e/playwright/basic-sketch.spec.ts',
},
{
name: ' - internal_engine',
stack: `
`,
message: `Nothing to export`,
project: 'Google Chrome',
foundInSpec: 'e2e/playwright/regression-tests.spec.ts',
},
{
name: 'SyntaxError',
stack: `SyntaxError: Unexpected end of JSON input
at crossPlatformFetch (http://localhost:3000/src/lib/crossPlatformFetch.ts:34:31)
at async sendTelemetry (http://localhost:3000/src/lib/textToCad.ts:179:3)`,
message: `Unexpected end of JSON input`,
project: 'Google Chrome',
foundInSpec: 'e2e/playwright/text-to-cad-tests.spec.ts',
},
{
name: '{"kind"',
stack: ``,
message: `engine","sourceRanges":[[0,0]],"msg":"Failed to wait for promise from engine: JsValue(\\"Force interrupt, executionIsStale, new AST requested\\")"}`,
project: 'Google Chrome',
foundInSpec: 'e2e/playwright/testing-settings.spec.ts',
},
]
const cleanString = (str: string) => str.replace(/[`"]/g, '')
const foundItem = whitelist.find(
(item) =>
cleanString(exception.name) === cleanString(item.name) &&
cleanString(exception.message).includes(cleanString(item.message))
)
return foundItem !== undefined
}

View File

@ -147,6 +147,9 @@ test.describe('Can export from electron app', () => {
const u = await getUtils(page) const u = await getUtils(page)
page.on('console', console.log) page.on('console', console.log)
await electronApp.context().addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
})
const pointOnModel = { x: 630, y: 280 } const pointOnModel = { x: 630, y: 280 }
@ -170,10 +173,10 @@ test.describe('Can export from electron app', () => {
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(15) .toBeLessThan(10)
}) })
const exportLocations: Array<Paths> = [] const exportLocations: Array<Paths> = []
@ -204,7 +207,7 @@ test.describe('Can export from electron app', () => {
}, },
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBe(477481) .toBe(477327)
// clean up output.gltf // clean up output.gltf
await fsp.rm('output.gltf') await fsp.rm('output.gltf')
@ -492,6 +495,10 @@ test(
await file.click() await file.click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(u.codeLocator).toContainText( await expect(u.codeLocator).toContainText(
'A mounting bracket for the Focusrite Scarlett Solo audio interface' 'A mounting bracket for the Focusrite Scarlett Solo audio interface'
) )
@ -849,10 +856,10 @@ const extrude001 = extrude(200, sketch001)`)
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(15) .toBeLessThan(10)
await expect(async () => { await expect(async () => {
await page.mouse.move(0, 0, { steps: 5 }) await page.mouse.move(0, 0, { steps: 5 })
@ -860,8 +867,8 @@ const extrude001 = extrude(200, sketch001)`)
await page.mouse.click(pointOnModel.x, pointOnModel.y) await page.mouse.click(pointOnModel.x, pointOnModel.y)
// check user can interact with model by checking it turns yellow // check user can interact with model by checking it turns yellow
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [180, 180, 137])) .poll(() => u.getGreatestPixDiff(pointOnModel, [176, 180, 132]))
.toBeLessThan(15) .toBeLessThan(10)
}).toPass({ timeout: 40_000, intervals: [1_000] }) }).toPass({ timeout: 40_000, intervals: [1_000] })
await page.getByTestId('app-logo').click() await page.getByTestId('app-logo').click()
@ -935,15 +942,24 @@ test(
await page.getByText('bracket').click() await page.getByText('bracket').click()
await u.waitForPageLoad() 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,
})
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(15) .toBeLessThan(10)
}) })
await test.step('Clicking the logo takes us back to the projects page / home', async () => { await test.step('Clicking the logo takes us back to the projects page / home', async () => {
@ -960,15 +976,24 @@ test(
await page.getByText('router-template-slate').click() await page.getByText('router-template-slate').click()
await u.waitForPageLoad() 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,
})
// gray at this pixel means the stream has loaded in the most // gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color) // user way we can verify it (pixel color)
await expect await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(15) .toBeLessThan(10)
}) })
await test.step('Opening the router-template project should load the stream', async () => { await test.step('Opening the router-template project should load the stream', async () => {
@ -1719,7 +1744,7 @@ test.describe('Renaming in the file tree', () => {
}) })
await test.step('Rename the folder', async () => { await test.step('Rename the folder', async () => {
await page.waitForTimeout(2000) await page.waitForTimeout(60000)
await folderToRename.click({ button: 'right' }) await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible() await expect(renameMenuItem).toBeVisible()
await renameMenuItem.click() await renameMenuItem.click()

View File

@ -11,8 +11,8 @@ import {
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates' import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {
@ -54,67 +54,6 @@ const sketch001 = startSketchAt([-0, -0])
const crypticErrorText = `ApiError` const crypticErrorText = `ApiError`
await expect(page.getByText(crypticErrorText).first()).toBeVisible() 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 }) => { test('executes on load', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -419,7 +358,6 @@ const sketch001 = startSketchAt([-0, -0])
await page.addInitScript( await page.addInitScript(
async ({ code }) => { async ({ code }) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
}, },
{ {
code: bracket, code: bracket,
@ -455,22 +393,20 @@ const sketch001 = startSketchAt([-0, -0])
await test.step('The second export is blocked', async () => { await test.step('The second export is blocked', async () => {
// Find the toast. // Find the toast.
// Look out for the toast message // Look out for the toast message
await Promise.all([ await expect(exportingToastMessage).toBeVisible()
expect(exportingToastMessage.first()).toBeVisible(), await expect(alreadyExportingToastMessage).toBeVisible()
expect(alreadyExportingToastMessage).toBeVisible(),
]) await page.waitForTimeout(1000)
}) })
await test.step('The first export still succeeds', async () => { await test.step('The first export still succeeds', async () => {
await Promise.all([ await expect(exportingToastMessage).not.toBeVisible()
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }), await expect(errorToastMessage).not.toBeVisible()
expect(errorToastMessage).not.toBeVisible(), await expect(engineErrorToastMessage).not.toBeVisible()
expect(engineErrorToastMessage).not.toBeVisible(),
expect(successToastMessage).toBeVisible({ timeout: 15_000 }), await expect(successToastMessage).toBeVisible()
expect(alreadyExportingToastMessage).not.toBeVisible({
timeout: 15_000, await expect(alreadyExportingToastMessage).not.toBeVisible()
}),
])
}) })
}) })
@ -483,12 +419,10 @@ const sketch001 = startSketchAt([-0, -0])
await expect(exportingToastMessage).toBeVisible() await expect(exportingToastMessage).toBeVisible()
// Expect it to succeed. // Expect it to succeed.
await Promise.all([ await expect(exportingToastMessage).not.toBeVisible()
expect(exportingToastMessage).not.toBeVisible(), await expect(errorToastMessage).not.toBeVisible()
expect(errorToastMessage).not.toBeVisible(), await expect(engineErrorToastMessage).not.toBeVisible()
expect(engineErrorToastMessage).not.toBeVisible(), await expect(alreadyExportingToastMessage).not.toBeVisible()
expect(alreadyExportingToastMessage).not.toBeVisible(),
])
await expect(successToastMessage).toBeVisible() await expect(successToastMessage).toBeVisible()
}) })

View File

@ -9,8 +9,8 @@ import {
} from './test-utils' } from './test-utils'
import { uuidv4, roundOff } from 'lib/utils' import { uuidv4, roundOff } from 'lib/utils'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -2,8 +2,8 @@ import { test, expect } from '@playwright/test'
import { commonPoints, getUtils, setup, tearDown } from './test-utils' import { commonPoints, getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {

View File

@ -26,9 +26,7 @@ import {
import * as TOML from '@iarna/toml' import * as TOML from '@iarna/toml'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes' import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants' import { SETTINGS_FILE_NAME } from 'lib/constants'
import { isErrorWhitelisted } from './lib/console-error-whitelist'
import { isArray } from 'lib/utils' import { isArray } from 'lib/utils'
import { reportRejection } from 'lib/trap'
type TestColor = [number, number, number] type TestColor = [number, number, number]
export const TEST_COLORS = { export const TEST_COLORS = {
@ -441,50 +439,46 @@ export async function getUtils(page: Page, test_?: typeof test) {
} }
return maxDiff return maxDiff
}, },
doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) => doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
new Promise<boolean>((resolve) => { new Promise(async (resolve) => {
;(async () => { await page.screenshot({
path: './e2e/playwright/temp1.png',
fullPage: true,
})
await fn()
const isImageDiff = async () => {
await page.screenshot({ await page.screenshot({
path: './e2e/playwright/temp1.png', path: './e2e/playwright/temp2.png',
fullPage: true, fullPage: true,
}) })
await fn() const screenshot1 = PNG.sync.read(
const isImageDiff = async () => { await fsp.readFile('./e2e/playwright/temp1.png')
await page.screenshot({ )
path: './e2e/playwright/temp2.png', const screenshot2 = PNG.sync.read(
fullPage: true, await fsp.readFile('./e2e/playwright/temp2.png')
}) )
const screenshot1 = PNG.sync.read( const actualDiffCount = pixelMatch(
await fsp.readFile('./e2e/playwright/temp1.png') screenshot1.data,
) screenshot2.data,
const screenshot2 = PNG.sync.read( null,
await fsp.readFile('./e2e/playwright/temp2.png') screenshot1.width,
) screenshot2.height
const actualDiffCount = pixelMatch( )
screenshot1.data, return actualDiffCount > diffCount
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) // run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
let count = 0 let count = 0
const interval = setInterval(() => { const interval = setInterval(async () => {
;(async () => { count++
count++ if (await isImageDiff()) {
if (await isImageDiff()) { clearInterval(interval)
clearInterval(interval) resolve(true)
resolve(true) } else if (count > 100) {
} else if (count > 100) { clearInterval(interval)
clearInterval(interval) resolve(false)
resolve(false) }
} }, 50)
})().catch(reportRejection)
}, 50)
})().catch(reportRejection)
}), }),
emulateNetworkConditions: async ( emulateNetworkConditions: async (
networkOptions: Protocol.Network.emulateNetworkConditionsParameters networkOptions: Protocol.Network.emulateNetworkConditionsParameters
@ -517,7 +511,10 @@ export async function getUtils(page: Page, test_?: typeof test) {
editorTextMatches: async (code: string) => { editorTextMatches: async (code: string) => {
const editor = page.locator(editorSelector) const editor = page.locator(editorSelector)
return expect(editor).toHaveText(code, { useInnerText: true }) const editorText = await editor.textContent()
return expect(util.toNormalizedCode(editorText || '')).toBe(
util.toNormalizedCode(code)
)
}, },
pasteCodeInEditor: async (code: string) => { pasteCodeInEditor: async (code: string) => {
@ -535,74 +532,18 @@ export async function getUtils(page: Page, test_?: typeof test) {
}) })
}, },
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) => { createNewFileAndSelect: async (name: string) => {
return test?.step(`Create a file named ${name}, select it`, async () => { return test?.step(`Create a file named ${name}, select it`, async () => {
await openFilePanel(page)
await page.getByTestId('create-file-button').click() await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name) await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
const newFile = page await page
.locator('[data-testid="file-pane-scroll-container"] button') .getByTestId('file-pane-scroll-container')
.filter({ hasText: name }) .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() .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[]) => { panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => { return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript( await page.addInitScript(
@ -823,11 +764,7 @@ export async function tearDown(page: Page, testInfo: TestInfo) {
// settingsOverrides may need to be augmented to take more generic items, // settingsOverrides may need to be augmented to take more generic items,
// but we'll be strict for now // but we'll be strict for now
export async function setup( export async function setup(context: BrowserContext, page: Page) {
context: BrowserContext,
page: Page,
testInfo?: TestInfo
) {
await context.addInitScript( await context.addInitScript(
async ({ token, settingsKey, settings, IS_PLAYWRIGHT_KEY }) => { async ({ token, settingsKey, settings, IS_PLAYWRIGHT_KEY }) => {
localStorage.clear() localStorage.clear()
@ -835,6 +772,7 @@ export async function setup(
localStorage.setItem('persistCode', ``) localStorage.setItem('persistCode', ``)
localStorage.setItem(settingsKey, settings) localStorage.setItem(settingsKey, settings)
localStorage.setItem(IS_PLAYWRIGHT_KEY, 'true') localStorage.setItem(IS_PLAYWRIGHT_KEY, 'true')
console.log('TEST_SETTINGS.projects', settings)
}, },
{ {
token: secrets.token, token: secrets.token,
@ -863,8 +801,6 @@ export async function setup(
secure: true, secure: true,
}, },
]) ])
failOnConsoleErrors(page, testInfo)
// kill animations, speeds up tests and reduced flakiness // kill animations, speeds up tests and reduced flakiness
await page.emulateMedia({ reducedMotion: 'reduce' }) await page.emulateMedia({ reducedMotion: 'reduce' })
@ -876,12 +812,10 @@ export async function setupElectron({
testInfo, testInfo,
folderSetupFn, folderSetupFn,
cleanProjectDir = true, cleanProjectDir = true,
appSettings,
}: { }: {
testInfo: TestInfo testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void> folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean cleanProjectDir?: boolean
appSettings?: Partial<SaveSettingsPayload>
}) { }) {
// create or otherwise clear the folder // create or otherwise clear the folder
const projectDirName = testInfo.outputPath('electron-test-projects-dir') const projectDirName = testInfo.outputPath('electron-test-projects-dir')
@ -915,19 +849,15 @@ export async function setupElectron({
if (cleanProjectDir) { if (cleanProjectDir) {
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME) const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify( const settingsOverrides = TOML.stringify({
appSettings ...TEST_SETTINGS,
? { settings: appSettings } settings: {
: { app: {
...TEST_SETTINGS, ...TEST_SETTINGS.app,
settings: { projectDirectory: projectDirName,
app: { },
...TEST_SETTINGS.app, },
projectDirectory: projectDirName, })
},
},
}
)
await fsp.writeFile(tempSettingsFilePath, settingsOverrides) await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
} }
@ -938,48 +868,6 @@ export async function setupElectron({
return { electronApp, page, dir: projectDirName } return { electronApp, page, dir: projectDirName }
} }
function failOnConsoleErrors(page: Page, testInfo?: TestInfo) {
// enabled for chrome for now
if (page.context().browser()?.browserType().name() === 'chromium') {
page.on('pageerror', (exception) => {
if (isErrorWhitelisted(exception)) {
return
}
// only set this env var to false if you want to collect console errors
// This can be configured in the GH workflow. This should be set to true by default (we want tests to fail when
// unwhitelisted console errors are detected).
if (process.env.FAIL_ON_CONSOLE_ERRORS === 'true') {
// Fail when running on CI and FAIL_ON_CONSOLE_ERRORS is set
// use expect to prevent page from closing and not cleaning up
expect(`An error was detected in the console: \r\n message:${exception.message} \r\n name:${exception.name} \r\n stack:${exception.stack}
*Either fix the console error or add it to the whitelist defined in ./lib/console-error-whitelist.ts (if the error can be safely ignored)
`).toEqual('Console error detected')
} else {
// the (test-results/exceptions.txt) file will be uploaded as part of an upload artifact in GH
fsp
.appendFile(
'./test-results/exceptions.txt',
[
'~~~',
`triggered_by_test:${
testInfo?.file + ' ' + (testInfo?.title || ' ')
}`,
`name:${exception.name}`,
`message:${exception.message}`,
`stack:${exception.stack}`,
`project:${testInfo?.project.name}`,
'~~~',
].join('\n')
)
.catch((err) => {
console.error(err)
})
}
})
}
}
export async function isOutOfViewInScrollContainer( export async function isOutOfViewInScrollContainer(
element: Locator, element: Locator,
container: Locator container: Locator

View File

@ -3,8 +3,8 @@ import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { getUtils, setup, tearDown } from './test-utils' import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {
@ -12,8 +12,8 @@ test.afterEach(async ({ page }, testInfo) => {
}) })
test.describe('Testing Camera Movement', () => { test.describe('Testing Camera Movement', () => {
test('Can move camera reliably', async ({ page, context }) => { test('Can moving camera', async ({ page, context }) => {
test.skip(process.platform === 'darwin', 'Can move camera reliably') test.skip(process.platform === 'darwin', 'Can moving camera')
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -102,13 +102,6 @@ test.describe('Testing Camera Movement', () => {
await bakeInRetries(async () => { await bakeInRetries(async () => {
await page.mouse.move(700, 200) await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' }) 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.move(600, 303)
await page.mouse.up({ button: 'right' }) await page.mouse.up({ button: 'right' })
}, [4, -10.5, -120]) }, [4, -10.5, -120])
@ -302,11 +295,11 @@ test.describe('Testing Camera Movement', () => {
await expect( await expect(
page.getByRole('button', { name: 'Edit Sketch' }) page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible() ).toBeVisible()
await hoverOverNothing()
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400) await page.waitForTimeout(400)
await hoverOverNothing()
x = 975 x = 975
y = 468 y = 468

View File

@ -3,8 +3,8 @@ import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown, TEST_COLORS } from './test-utils' import { getUtils, setup, tearDown, TEST_COLORS } from './test-utils'
import { XOR } from 'lib/utils' import { XOR } from 'lib/utils'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {

View File

@ -4,8 +4,8 @@ import { getUtils, setup, tearDown } from './test-utils'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { TEST_CODE_GIZMO } from './storageStates' import { TEST_CODE_GIZMO } from './storageStates'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {

View File

@ -4,8 +4,8 @@ import { deg, getUtils, setup, tearDown, wiggleMove } from './test-utils'
import { LineInputsType } from 'lang/std/sketchcombos' import { LineInputsType } from 'lang/std/sketchcombos'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {

View File

@ -5,8 +5,8 @@ import { Coords2d } from 'lang/std/sketch'
import { KCL_DEFAULT_LENGTH } from 'lib/constants' import { KCL_DEFAULT_LENGTH } from 'lib/constants'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {
@ -31,18 +31,8 @@ test.describe('Testing selections', () => {
const xAxisClick = () => const xAxisClick = () =>
page.mouse.click(700, 253).then(() => page.waitForTimeout(100)) 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 = () => const emptySpaceClick = () =>
test.step(`Click in empty space`, async () => { page.mouse.click(700, 343).then(() => page.waitForTimeout(100))
await page.mouse.click(700, 143)
await expect(page.locator('.cm-line').last()).toHaveClass(
/cm-activeLine/
)
})
const topHorzSegmentClick = () => const topHorzSegmentClick = () =>
page.mouse.click(709, 290).then(() => page.waitForTimeout(100)) page.mouse.click(709, 290).then(() => page.waitForTimeout(100))
const bottomHorzSegmentClick = () => const bottomHorzSegmentClick = () =>
@ -181,9 +171,7 @@ test.describe('Testing selections', () => {
await emptySpaceClick() await emptySpaceClick()
} }
await test.step(`Test hovering and selecting on fresh sketch`, async () => { await selectionSequence()
await selectionSequence()
})
// hovering in fresh sketch worked, lets try exiting and re-entering // hovering in fresh sketch worked, lets try exiting and re-entering
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
@ -196,15 +184,16 @@ test.describe('Testing selections', () => {
// select a line, this verifies that sketches in the scene can be selected outside of sketch mode // select a line, this verifies that sketches in the scene can be selected outside of sketch mode
await topHorzSegmentClick() await topHorzSegmentClick()
await emptySpaceHover() await page.waitForTimeout(100)
// enter sketch again // enter sketch again
await u.doAndWaitForCmd( await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Edit Sketch' }).click(), () => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings' 'default_camera_get_settings'
) )
await page.waitForTimeout(150)
await page.waitForTimeout(450) // wait for animation await page.waitForTimeout(300) // wait for animation
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.sendCustomCmd({ await u.sendCustomCmd({
@ -231,9 +220,8 @@ test.describe('Testing selections', () => {
await u.closeDebugPanel() await u.closeDebugPanel()
await test.step(`Test hovering and selecting on edited sketch`, async () => { // hover again and check it works
await selectionSequence() await selectionSequence()
})
}) })
test('Solids should be select and deletable', async ({ page }) => { test('Solids should be select and deletable', async ({ page }) => {
@ -785,9 +773,9 @@ const extrude001 = extrude(50, sketch001)
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
let noHoverColor: [number, number, number] = [92, 92, 92] let noHoverColor: [number, number, number] = [82, 82, 82]
let hoverColor: [number, number, number] = [127, 127, 127] let hoverColor: [number, number, number] = [116, 116, 116]
let selectColor: [number, number, number] = [155, 155, 105] let selectColor: [number, number, number] = [144, 148, 97]
const extrudeWall = { x: 670, y: 275 } const extrudeWall = { x: 670, y: 275 }
const extrudeText = `line([170.36, -121.61], %, $seg01)` const extrudeText = `line([170.36, -121.61], %, $seg01)`
@ -799,7 +787,7 @@ const extrude001 = extrude(50, sketch001)
await expect await expect
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor)) .poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
.toBeLessThan(15) .toBeLessThan(5)
await page.mouse.move(nothing.x, nothing.y) await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.move(extrudeWall.x, extrudeWall.y) await page.mouse.move(extrudeWall.x, extrudeWall.y)
@ -810,43 +798,43 @@ const extrude001 = extrude(50, sketch001)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect( await expect(
await u.getGreatestPixDiff(extrudeWall, hoverColor) await u.getGreatestPixDiff(extrudeWall, hoverColor)
).toBeLessThan(15) ).toBeLessThan(6)
await page.mouse.click(extrudeWall.x, extrudeWall.y) await page.mouse.click(extrudeWall.x, extrudeWall.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`) await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect( await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor) await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(15) ).toBeLessThan(6)
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously) // check color stays there, i.e. not overridden (this was a bug previously)
await expect( await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor) await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(15) ).toBeLessThan(6)
await page.mouse.move(nothing.x, nothing.y) await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(300) await page.waitForTimeout(300)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
// because of shading, color is not exact everywhere on the face // because of shading, color is not exact everywhere on the face
noHoverColor = [115, 115, 115] noHoverColor = [104, 104, 104]
hoverColor = [145, 145, 145] hoverColor = [134, 134, 134]
selectColor = [168, 168, 120] selectColor = [158, 162, 110]
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(15) await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(6)
await page.mouse.move(cap.x, cap.y) await page.mouse.move(cap.x, cap.y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible() await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
await expect(page.getByTestId('hover-highlight').first()).toContainText( await expect(page.getByTestId('hover-highlight').first()).toContainText(
removeAfterFirstParenthesis(capText) removeAfterFirstParenthesis(capText)
) )
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15) await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(6)
await page.mouse.click(cap.x, cap.y) await page.mouse.click(cap.x, cap.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`) await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15) await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously) // check color stays there, i.e. not overridden (this was a bug previously)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15) await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
}) })
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({ test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
page, page,

View File

@ -8,16 +8,12 @@ import {
tearDown, tearDown,
executorInputPath, executorInputPath,
} from './test-utils' } from './test-utils'
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes' import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates'
TEST_SETTINGS_KEY,
TEST_SETTINGS_CORRUPTED,
TEST_SETTINGS,
} from './storageStates'
import * as TOML from '@iarna/toml' import * as TOML from '@iarna/toml'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {
@ -158,33 +154,29 @@ test.describe('Testing settings', () => {
test('Project and user settings can be reset', async ({ page }) => { test('Project and user settings can be reset', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await test.step(`Setup`, async () => { await page.setViewportSize({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 }) await u.waitForAuthSkipAppStart()
await u.waitForAuthSkipAppStart() await page
}) .getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' })
// Selectors and constants
const projectSettingsTab = page.getByRole('radio', { name: 'Project' }) const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
const userSettingsTab = page.getByRole('radio', { name: 'User' }) const userSettingsTab = page.getByRole('radio', { name: 'User' })
const resetButton = (level: SettingsLevel) => const resetButton = page.getByRole('button', {
page.getByRole('button', { name: 'Restore default settings',
name: `Reset ${level}-level settings`, })
})
const themeColorSetting = page.locator('#themeColor').getByRole('slider') const themeColorSetting = page.locator('#themeColor').getByRole('slider')
const settingValues = { const settingValues = {
default: '259', default: '259',
user: '120', user: '120',
project: '50', project: '50',
} }
const resetToast = (level: SettingsLevel) =>
page.getByText(`${level}-level settings were reset`)
await test.step(`Open the settings modal`, async () => { // Open the settings modal with lower-right button
await page.getByRole('link', { name: 'Settings' }).last().click() await page.getByRole('link', { name: 'Settings' }).last().click()
await expect( await expect(
page.getByRole('heading', { name: 'Settings', exact: true }) page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible() ).toBeVisible()
})
await test.step('Set up theme color', async () => { await test.step('Set up theme color', async () => {
// Verify we're looking at the project-level settings, // Verify we're looking at the project-level settings,
@ -203,40 +195,37 @@ test.describe('Testing settings', () => {
await test.step('Reset project settings', async () => { await test.step('Reset project settings', async () => {
// Click the reset settings button. // Click the reset settings button.
await resetButton('project').click() await resetButton.click()
await expect(resetToast('project')).toBeVisible() await expect(page.getByText('Settings restored to default')).toBeVisible()
await expect(resetToast('project')).not.toBeVisible() await expect(
page.getByText('Settings restored to default')
).not.toBeVisible()
// Verify it is now set to the inherited user value // Verify it is now set to the inherited user value
await expect(themeColorSetting).toHaveValue(settingValues.user) await expect(themeColorSetting).toHaveValue(settingValues.default)
await test.step(`Check that the user settings did not change`, async () => { // Check that the user setting also rolled back
await userSettingsTab.click() await userSettingsTab.click()
await expect(themeColorSetting).toHaveValue(settingValues.user) await expect(themeColorSetting).toHaveValue(settingValues.default)
}) await projectSettingsTab.click()
await test.step(`Set project-level again to test the user-level reset`, async () => { // Set project-level value to 50 again to test the user-level reset
await projectSettingsTab.click() await themeColorSetting.fill(settingValues.project)
await themeColorSetting.fill(settingValues.project) await userSettingsTab.click()
await userSettingsTab.click()
})
}) })
await test.step('Reset user settings', async () => { await test.step('Reset user settings', async () => {
// Click the reset settings button. // Change the setting and click the reset settings button.
await resetButton('user').click() await themeColorSetting.fill(settingValues.user)
await resetButton.click()
await expect(resetToast('user')).toBeVisible()
await expect(resetToast('user')).not.toBeVisible()
// Verify it is now set to the default value // Verify it is now set to the default value
await expect(themeColorSetting).toHaveValue(settingValues.default) await expect(themeColorSetting).toHaveValue(settingValues.default)
await test.step(`Check that the project settings did not change`, async () => { // Check that the project setting also changed
await projectSettingsTab.click() await projectSettingsTab.click()
await expect(themeColorSetting).toHaveValue(settingValues.project) await expect(themeColorSetting).toHaveValue(settingValues.default)
})
}) })
}) })
@ -299,7 +288,7 @@ test.describe('Testing settings', () => {
}) })
await test.step('Refresh the application and see project setting applied', async () => { await test.step('Refresh the application and see project setting applied', async () => {
await page.reload({ waitUntil: 'domcontentloaded' }) await page.reload()
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor) await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
await settingsCloseButton.click() await settingsCloseButton.click()
@ -314,109 +303,53 @@ test.describe('Testing settings', () => {
} }
) )
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( test(
`Closing settings modal should go back to the original file being viewed`, `Closing settings modal should go back to the original file being viewed`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async () => {},
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 { const {
openKclCodePanel, panesOpen,
openFilePanel, createAndSelectProject,
waitForPageLoad, pasteCodeInEditor,
selectFile, clickPane,
createNewFileAndSelect,
editorTextMatches, editorTextMatches,
} = await getUtils(page, test) } = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await test.step('Precondition: Open to second project file', async () => { await panesOpen([])
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 test.step('Precondition: No projects exist', async () => {
await editorTextMatches(kclCylinder) await expect(page.getByTestId('home-section')).toBeVisible()
const projectLinksPre = page.getByTestId('project-link')
await expect(projectLinksPre).toHaveCount(0)
}) })
await createAndSelectProject('project-000')
await clickPane('code')
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCube)
await clickPane('files')
await createNewFileAndSelect('2.kcl')
const kclCylinder = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCylinder)
const settingsOpenButton = page.getByRole('link', { const settingsOpenButton = page.getByRole('link', {
name: 'settings Settings', name: 'settings Settings',
}) })
@ -424,9 +357,6 @@ test.describe('Testing settings', () => {
await test.step('Open and close settings', async () => { await test.step('Open and close settings', async () => {
await settingsOpenButton.click() await settingsOpenButton.click()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await settingsCloseButton.click() await settingsCloseButton.click()
}) })
@ -440,37 +370,25 @@ test.describe('Testing settings', () => {
test('Changing modeling default unit', async ({ page }) => { test('Changing modeling default unit', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await test.step(`Test setup`, async () => { await page.setViewportSize({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 }) await u.waitForAuthSkipAppStart()
await u.waitForAuthSkipAppStart() await page
await page .getByRole('button', { name: 'Start Sketch' })
.getByRole('button', { name: 'Start Sketch' }) .waitFor({ state: 'visible' })
.waitFor({ state: 'visible' })
})
// Selectors and constants
const userSettingsTab = page.getByRole('radio', { name: 'User' }) 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 () => { // Open the settings modal with lower-right button
await page.getByRole('link', { name: 'Settings' }).last().click() await page.getByRole('link', { name: 'Settings' }).last().click()
await expect( await expect(
page.getByRole('heading', { name: 'Settings', exact: true }) page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible() ).toBeVisible()
})
await test.step(`Reset unit setting`, async () => { const resetButton = page.getByRole('button', {
await userSettingsTab.click() name: 'Restore default settings',
await defaultUnitSection.hover()
await defaultUnitRollbackButton.click()
await projectSettingsTab.click()
}) })
// Default unit should be mm
await resetButton.click()
await test.step('Change modeling default unit within project tab', async () => { await test.step('Change modeling default unit within project tab', async () => {
const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => { const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => {
@ -575,148 +493,4 @@ test.describe('Testing settings', () => {
await changeUnitOfMeasureInGizmo('m', 'Meters') 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()
})
})
}) })

View File

@ -1,5 +1,11 @@
import { test, expect, Page } from '@playwright/test' import { test, expect, Page } from '@playwright/test'
import { getUtils, setup, tearDown, setupElectron } from './test-utils' import {
getUtils,
setup,
tearDown,
setupElectron,
createProjectAndRenameIt,
} from './test-utils'
import { join } from 'path' import { join } from 'path'
import fs from 'fs' import fs from 'fs'
@ -534,7 +540,7 @@ test.describe('Text-to-CAD tests', () => {
// Ensure the final toast remains. // Ensure the final toast remains.
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible() await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
await expect(page.getByText(`Prompt: "a 2x8 lego`)).not.toBeVisible() await expect(page.getByText(`a 2x8 lego`)).not.toBeVisible()
await expect(page.getByText(`a 2x4 lego`)).toBeVisible() await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
// Ensure you can copy the code for the final model. // Ensure you can copy the code for the final model.
@ -690,53 +696,41 @@ test(
'Text-to-CAD functionality', 'Text-to-CAD functionality',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
const projectName = 'project-000'
const prompt = 'lego 2x4'
const textToCadFileName = 'lego-2x4.kcl'
const { electronApp, page, dir } = await setupElectron({ testInfo }) const { electronApp, page, dir } = await setupElectron({ testInfo })
const fileExists = () => const fileExists = () =>
fs.existsSync(join(dir, projectName, textToCadFileName)) fs.existsSync(join(dir, 'test-000', 'lego-2x4.kcl'))
const {
createAndSelectProject,
openFilePanel,
openKclCodePanel,
waitForPageLoad,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 }) 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 // Create and navigate to the project
await createAndSelectProject('project-000') await createProjectAndRenameIt({ name: 'test-000', page })
await page.getByTestId('project-link').click()
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command // Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await waitForPageLoad() await expect(
await openFilePanel() page.getByRole('button', { name: 'Start Sketch' })
await openKclCodePanel() ).toBeEnabled({
timeout: 20_000,
})
// Open the files pane
const filesPaneButton = page.getByTestId('files-pane-button')
await filesPaneButton.click()
await test.step(`Test file creation`, async () => { await test.step(`Test file creation`, async () => {
await sendPromptFromCommandBar(page, prompt) await sendPromptFromCommandBar(page, 'lego 2x4')
// File is considered created if it shows up in the Project Files pane // File is considered created if it shows up in the Project Files pane
await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 }) const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await expect(file).toBeVisible({ timeout: 20_000 })
expect(fileExists()).toBeTruthy() expect(fileExists()).toBeTruthy()
}) })
await test.step(`Test file navigation`, async () => { await test.step(`Test file navigation`, async () => {
await expect(projectMenuButton).toContainText('main.kcl') const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await textToCadFileButton.click() await file.click()
const kclComment = page.getByText('Lego 2x4 Brick')
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane // 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(kclComment).toBeVisible({ timeout: 20_000 })
await expect(projectMenuButton).toContainText(textToCadFileName)
}) })
await test.step(`Test file deletion on rejection`, async () => { await test.step(`Test file deletion on rejection`, async () => {
@ -750,8 +744,6 @@ test(
) )
await expect(submittingToastMessage).toBeVisible() await expect(submittingToastMessage).toBeVisible()
expect(fileExists()).toBeFalsy() expect(fileExists()).toBeFalsy()
// Confirm we've navigated back to the main.kcl file after deletion
await expect(projectMenuButton).toContainText('main.kcl')
}) })
await electronApp.close() await electronApp.close()

View File

@ -2,8 +2,8 @@ import { test, expect } from '@playwright/test'
import { doExport, getUtils, makeTemplate, setup, tearDown } from './test-utils' import { doExport, getUtils, makeTemplate, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page, testInfo) await setup(context, page)
}) })
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {

View File

@ -1,75 +0,0 @@
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

View File

@ -15,7 +15,6 @@
/> />
<link rel="apple-touch-icon" href="/logo192.png" /> <link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="./inter/inter.css" />
<link rel="stylesheet" href="https://use.typekit.net/zzv8rvm.css" /> <link rel="stylesheet" href="https://use.typekit.net/zzv8rvm.css" />
<script <script
defer defer

View File

@ -1,8 +0,0 @@
!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

2
interface.d.ts vendored
View File

@ -30,6 +30,8 @@ export interface IElectronAPI {
join: typeof path.join join: typeof path.join
sep: typeof path.sep sep: typeof path.sep
rename: (prev: string, next: string) => typeof fs.rename rename: (prev: string, next: string) => typeof fs.rename
setBaseUrl: (value: string) => void
loadProjectAtStartup: () => Promise<ProjectState | null>
packageJson: { packageJson: {
name: string name: string
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "zoo-modeling-app", "name": "zoo-modeling-app",
"version": "0.25.2", "version": "0.24.12",
"private": true, "private": true,
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"author": { "author": {
@ -34,12 +34,11 @@
"@ts-stack/markdown": "^1.5.0", "@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1", "@tweenjs/tween.js": "^23.1.1",
"@xstate/inspect": "^0.8.0", "@xstate/inspect": "^0.8.0",
"@xstate/react": "^4.1.1", "@xstate/react": "^3.2.2",
"bonjour-service": "^1.2.1", "bonjour-service": "^1.2.1",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1", "electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.3.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
@ -51,7 +50,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.5.1", "react-hotkeys-hook": "^4.5.0",
"react-json-view": "^1.21.3", "react-json-view": "^1.21.3",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2", "react-modal-promise": "^1.0.2",
@ -64,7 +63,7 @@
"vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-protocol": "^3.17.5",
"vscode-uri": "^3.0.8", "vscode-uri": "^3.0.8",
"web-vitals": "^3.5.2", "web-vitals": "^3.5.2",
"xstate": "^5.17.4" "xstate": "^4.38.2"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@ -88,7 +87,7 @@
"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", "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\"", "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": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings", "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", "lint": "eslint --fix src e2e",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json", "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild", "postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"", "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
@ -98,9 +97,7 @@
"tron:package": "electron-forge package", "tron:package": "electron-forge package",
"tron:make": "electron-forge make", "tron:make": "electron-forge make",
"tron:publish": "electron-forge publish", "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": { "prettier": {
"trailingComma": "es5", "trailingComma": "es5",
@ -153,6 +150,7 @@
"@types/three": "^0.163.0", "@types/three": "^0.163.0",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/wait-on": "^5.3.4",
"@types/wicg-file-system-access": "^2023.10.5", "@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
@ -163,12 +161,10 @@
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"d3-force": "^3.0.0", "d3-force": "^3.0.0",
"electron": "^32.0.1", "electron": "^32.0.1",
"electron-builder": "^24.13.3",
"electron-notarize": "^1.2.2",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.30.0", "eslint-plugin-import": "^2.25.0",
"eslint-plugin-suggest-no-throw": "^1.0.0", "eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10", "happy-dom": "^14.3.10",
"http-server": "^14.1.1", "http-server": "^14.1.1",
@ -176,7 +172,7 @@
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"pixelmatch": "^5.3.0", "pixelmatch": "^5.3.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"postcss": "^8.4.43", "postcss": "^8.4.31",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"setimmediate": "^1.0.5", "setimmediate": "^1.0.5",
@ -189,6 +185,7 @@
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"vitest-webgl-canvas-mock": "^1.1.0", "vitest-webgl-canvas-mock": "^1.1.0",
"wait-on": "^7.2.0",
"wasm-pack": "^0.13.0", "wasm-pack": "^0.13.0",
"ws": "^8.17.0", "ws": "^8.17.0",
"yarn": "^1.22.22" "yarn": "^1.22.22"

View File

@ -72,7 +72,6 @@ export class LanguageServerClient {
async initialize() { async initialize() {
// Start the client in the background. // Start the client in the background.
this.client.setNotifyFn(this.processNotifications.bind(this)) this.client.setNotifyFn(this.processNotifications.bind(this))
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.client.start() this.client.start()
this.ready = true this.ready = true
@ -196,9 +195,6 @@ export class LanguageServerClient {
} }
private processNotifications(notification: LSP.NotificationMessage) { private processNotifications(notification: LSP.NotificationMessage) {
for (const plugin of this.plugins) { for (const plugin of this.plugins) plugin.processNotification(notification)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
plugin.processNotification(notification)
}
} }
} }

View File

@ -12,7 +12,6 @@ export default function lspFormatExt(
run: (view: EditorView) => { run: (view: EditorView) => {
let value = view.plugin(plugin) let value = view.plugin(plugin)
if (!value) return false if (!value) return false
// eslint-disable-next-line @typescript-eslint/no-floating-promises
value.requestFormatting() value.requestFormatting()
return true return true
}, },

View File

@ -117,7 +117,6 @@ export class LanguageServerPlugin implements PluginValue {
this.processLspNotification = options.processLspNotification this.processLspNotification = options.processLspNotification
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.initialize({ this.initialize({
documentText: this.getDocText(), documentText: this.getDocText(),
}) })
@ -150,7 +149,6 @@ export class LanguageServerPlugin implements PluginValue {
} }
async initialize({ documentText }: { documentText: string }) { async initialize({ documentText }: { documentText: string }) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (this.client.initializePromise) { if (this.client.initializePromise) {
await this.client.initializePromise await this.client.initializePromise
} }
@ -164,9 +162,7 @@ export class LanguageServerPlugin implements PluginValue {
}, },
}) })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.requestSemanticTokens() this.requestSemanticTokens()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateFoldingRanges() this.updateFoldingRanges()
} }
@ -229,9 +225,7 @@ export class LanguageServerPlugin implements PluginValue {
contentChanges: [{ text: this.view.state.doc.toString() }], contentChanges: [{ text: this.view.state.doc.toString() }],
}) })
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.requestSemanticTokens() this.requestSemanticTokens()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateFoldingRanges() this.updateFoldingRanges()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@ -532,9 +526,7 @@ export class LanguageServerPlugin implements PluginValue {
processDiagnostics(params: PublishDiagnosticsParams) { processDiagnostics(params: PublishDiagnosticsParams) {
if (params.uri !== this.getDocUri()) return if (params.uri !== this.getDocUri()) return
// Commented to avoid the lint. See TODO below. const diagnostics = params.diagnostics
// const diagnostics =
params.diagnostics
.map(({ range, message, severity }) => ({ .map(({ range, message, severity }) => ({
from: posToOffset(this.view.state.doc, range.start)!, from: posToOffset(this.view.state.doc, range.start)!,
to: posToOffset(this.view.state.doc, range.end)!, to: posToOffset(this.view.state.doc, range.end)!,

View File

@ -0,0 +1,31 @@
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',
},
})

View File

@ -57,10 +57,10 @@ export default defineConfig({
}, },
}, // or 'chrome-beta' }, // or 'chrome-beta'
}, },
// { {
// name: 'webkit', name: 'webkit',
// use: { ...devices['Desktop Safari'] }, use: { ...devices['Desktop Safari'] },
// }, },
// { // {
// name: 'firefox', // name: 'firefox',
// use: { ...devices['Desktop Firefox'] }, // use: { ...devices['Desktop Firefox'] },

Binary file not shown.

View File

@ -1,14 +0,0 @@
@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");
}

Binary file not shown.

Binary file not shown.

View File

@ -1,38 +0,0 @@
// 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)
}
}

View File

@ -1,8 +1,12 @@
import { useEffect, useMemo, useRef } from 'react' import { MouseEventHandler, useEffect, useMemo, useRef } from 'react'
import { uuidv4 } from 'lib/utils'
import { useHotKeyListener } from './hooks/useHotKeyListener' import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream' import { Stream } from './components/Stream'
import { EngineCommand } from 'lang/std/artifactGraph'
import { throttle } from './lib/utils'
import { AppHeader } from './components/AppHeader' import { AppHeader } from './components/AppHeader'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { getNormalisedCoordinates } from './lib/utils'
import { useLoaderData, useNavigate } from 'react-router-dom' import { useLoaderData, useNavigate } from 'react-router-dom'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
@ -10,6 +14,7 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions' import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
import { codeManager, engineCommandManager } from 'lib/singletons' import { codeManager, engineCommandManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
@ -39,6 +44,7 @@ export function App() {
}, [projectName, projectPath]) }, [projectName, projectPath])
useHotKeyListener() useHotKeyListener()
const { context, state } = useModelingContext()
const { auth, settings } = useSettingsAuthContext() const { auth, settings } = useSettingsAuthContext()
const token = auth?.context?.token const token = auth?.context?.token
@ -67,14 +73,52 @@ export function App() {
(p) => p === onboardingStatus.current (p) => p === onboardingStatus.current
) )
? 'opacity-20' ? 'opacity-20'
: context.store?.didDragInStream
? 'opacity-40'
: '' : ''
useEngineConnectionSubscriptions() 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 ( return (
<div className="relative h-full flex flex-col" ref={ref}> <div
className="relative h-full flex flex-col"
onMouseMove={handleMouseMove}
ref={ref}
>
<AppHeader <AppHeader
className={'transition-opacity transition-duration-75 ' + paneOpacity} className={
'transition-opacity transition-duration-75 ' +
paneOpacity +
(context.store?.buttonDownInStream ? ' pointer-events-none' : '')
}
project={{ project, file }} project={{ project, file }}
enableMenu={true} enableMenu={true}
/> />

View File

@ -41,7 +41,6 @@ import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm' import { coreDump } from 'lang/wasm'
import { useMemo } from 'react' import { useMemo } from 'react'
import { AppStateProvider } from 'AppState' import { AppStateProvider } from 'AppState'
import { reportRejection } from 'lib/trap'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -70,6 +69,19 @@ const router = createRouter([
path: PATHS.INDEX, path: PATHS.INDEX,
loader: async () => { loader: async () => {
const onDesktop = isDesktop() const onDesktop = isDesktop()
if (onDesktop) {
const projectStartupFile =
await window.electron.loadProjectAtStartup()
if (projectStartupFile !== null) {
// Redirect to the file if we have a file path.
if (projectStartupFile.length > 0) {
return redirect(
PATHS.FILE + '/' + encodeURIComponent(projectStartupFile)
)
}
}
}
return onDesktop return onDesktop
? redirect(PATHS.HOME) ? redirect(PATHS.HOME)
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME) : redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
@ -174,23 +186,21 @@ function CoreDump() {
[] []
) )
useHotkeyWrapper(['mod + shift + .'], () => { useHotkeyWrapper(['mod + shift + .'], () => {
toast toast.promise(
.promise( coreDump(coreDumpManager, true),
coreDump(coreDumpManager, true), {
{ loading: 'Starting core dump...',
loading: 'Starting core dump...', success: 'Core dump completed successfully',
success: 'Core dump completed successfully', error: 'Error while exporting core dump',
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,
}, },
{ }
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 return null
} }

View File

@ -20,8 +20,6 @@ import {
ToolbarItemResolved, ToolbarItemResolved,
ToolbarModeName, ToolbarModeName,
} from 'lib/toolbar' } from 'lib/toolbar'
import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -70,12 +68,12 @@ export function Toolbar({
*/ */
const configCallbackProps: ToolbarItemCallbackProps = useMemo( const configCallbackProps: ToolbarItemCallbackProps = useMemo(
() => ({ () => ({
modelingState: state, modelingStateMatches: state.matches,
modelingSend: send, modelingSend: send,
commandBarSend, commandBarSend,
sketchPathId, sketchPathId,
}), }),
[state, send, commandBarSend, sketchPathId] [state.matches, send, commandBarSend, sketchPathId]
) )
/** /**
@ -290,11 +288,6 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
return ( return (
<Tooltip <Tooltip
inert={false} inert={false}
wrapperStyle={
isDesktop()
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
: {}
}
position="bottom" position="bottom"
wrapperClassName="!p-4 !pointer-events-auto" wrapperClassName="!p-4 !pointer-events-auto"
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch" contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
@ -344,7 +337,6 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
<li key={link.label} className="contents"> <li key={link.label} className="contents">
<a <a
href={link.url} href={link.url}
onClick={openExternalBrowserIfDesktop(link.url)}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit" className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"

View File

@ -22,12 +22,11 @@ import {
UnreliableSubscription, UnreliableSubscription,
} from 'lang/std/engineConnection' } from 'lang/std/engineConnection'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import { toSync, uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { deg2Rad } from 'lib/utils2d' import { deg2Rad } from 'lib/utils2d'
import { isReducedMotion, roundOff, throttle } from 'lib/utils' import { isReducedMotion, roundOff, throttle } from 'lib/utils'
import * as TWEEN from '@tweenjs/tween.js' import * as TWEEN from '@tweenjs/tween.js'
import { isQuaternionVertical } from './helpers' import { isQuaternionVertical } from './helpers'
import { reportRejection } from 'lib/trap'
const ORTHOGRAPHIC_CAMERA_SIZE = 20 const ORTHOGRAPHIC_CAMERA_SIZE = 20
const FRAMES_TO_ANIMATE_IN = 30 const FRAMES_TO_ANIMATE_IN = 30
@ -101,7 +100,6 @@ export class CameraControls {
camProps.type === 'perspective' && camProps.type === 'perspective' &&
this.camera instanceof OrthographicCamera this.camera instanceof OrthographicCamera
) { ) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
} else if ( } else if (
camProps.type === 'orthographic' && camProps.type === 'orthographic' &&
@ -129,7 +127,6 @@ export class CameraControls {
} }
throttledEngCmd = throttle((cmd: EngineCommand) => { throttledEngCmd = throttle((cmd: EngineCommand) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand(cmd) this.engineCommandManager.sendSceneCommand(cmd)
}, 1000 / 30) }, 1000 / 30)
@ -142,7 +139,6 @@ export class CameraControls {
...convertThreeCamValuesToEngineCam(threeValues), ...convertThreeCamValuesToEngineCam(threeValues),
}, },
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand(cmd) this.engineCommandManager.sendSceneCommand(cmd)
}, 1000 / 15) }, 1000 / 15)
@ -155,7 +151,6 @@ export class CameraControls {
this.lastPerspectiveCmd && this.lastPerspectiveCmd &&
Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay
) { ) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true) this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true)
this.lastPerspectiveCmdTime = Date.now() this.lastPerspectiveCmdTime = Date.now()
} }
@ -223,7 +218,6 @@ export class CameraControls {
this.useOrthographicCamera() this.useOrthographicCamera()
} }
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) { if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
} }
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) { if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
@ -255,7 +249,6 @@ export class CameraControls {
const doZoom = () => { const doZoom = () => {
if (this.zoomDataFromLastFrame !== undefined) { if (this.zoomDataFromLastFrame !== undefined) {
this.handleStart() this.handleStart()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({ this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
@ -273,7 +266,6 @@ export class CameraControls {
const doMove = () => { const doMove = () => {
if (this.moveDataFromLastFrame !== undefined) { if (this.moveDataFromLastFrame !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({ this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
@ -343,8 +335,7 @@ export class CameraControls {
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
} }
onMouseDown = (event: PointerEvent) => { onMouseDown = (event: MouseEvent) => {
this.domElement.setPointerCapture(event.pointerId)
this.isDragging = true this.isDragging = true
this.mouseDownPosition.set(event.clientX, event.clientY) this.mouseDownPosition.set(event.clientX, event.clientY)
let interaction = this.getInteractionType(event) let interaction = this.getInteractionType(event)
@ -364,7 +355,7 @@ export class CameraControls {
} }
} }
onMouseMove = (event: PointerEvent) => { onMouseMove = (event: MouseEvent) => {
if (this.isDragging) { if (this.isDragging) {
this.mouseNewPosition.set(event.clientX, event.clientY) this.mouseNewPosition.set(event.clientX, event.clientY)
const deltaMove = this.mouseNewPosition const deltaMove = this.mouseNewPosition
@ -402,29 +393,10 @@ export class CameraControls {
this.pendingPan.x += -deltaMove.x * panSpeed this.pendingPan.x += -deltaMove.x * panSpeed
this.pendingPan.y += deltaMove.y * panSpeed this.pendingPan.y += deltaMove.y * panSpeed
} }
} else {
/**
* If we're not in sketch mode and not dragging, we can highlight entities
* under the cursor. This recently moved from being handled in App.tsx.
* This might not be the right spot, but it is more consolidated.
*/
if (this.syncDirection === 'engineToClient') {
const newCmdId = uuidv4()
this.throttledEngCmd({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x: event.clientX, y: event.clientY },
},
cmd_id: newCmdId,
})
}
} }
} }
onMouseUp = (event: PointerEvent) => { onMouseUp = (event: MouseEvent) => {
this.domElement.releasePointerCapture(event.pointerId)
this.isDragging = false this.isDragging = false
this.handleEnd() this.handleEnd()
if (this.syncDirection === 'engineToClient') { if (this.syncDirection === 'engineToClient') {
@ -487,7 +459,6 @@ export class CameraControls {
this.camera.quaternion.set(qx, qy, qz, qw) this.camera.quaternion.set(qx, qy, qz, qw)
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.engineCommandManager.sendSceneCommand({ this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
@ -570,7 +541,7 @@ export class CameraControls {
const oldFov = this.camera.fov const oldFov = this.camera.fov
const viewHeightFactor = (fov: number) => { const viewHeightFactor = (fov: number) => {
/* * /* *
/| /|
/ | / |
/ | / |
@ -958,7 +929,6 @@ export class CameraControls {
} }
if (isReducedMotion()) { if (isReducedMotion()) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
onComplete() onComplete()
return return
} }
@ -967,7 +937,7 @@ export class CameraControls {
.to({ t: tweenEnd }, duration) .to({ t: tweenEnd }, duration)
.easing(TWEEN.Easing.Quadratic.InOut) .easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(({ t }) => cameraAtTime(t)) .onUpdate(({ t }) => cameraAtTime(t))
.onComplete(toSync(onComplete, reportRejection)) .onComplete(onComplete)
.start() .start()
}) })
} }
@ -992,7 +962,6 @@ export class CameraControls {
// Decrease the FOV // Decrease the FOV
currentFov = Math.max(currentFov - fovAnimationStep, targetFov) currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.dollyZoom(currentFov) this.dollyZoom(currentFov)
requestAnimationFrame(animateFovChange) // Continue the animation requestAnimationFrame(animateFovChange) // Continue the animation
} else if (frameWaitOnFinish > 0) { } else if (frameWaitOnFinish > 0) {
@ -1022,7 +991,6 @@ export class CameraControls {
this.lastPerspectiveFov = 4 this.lastPerspectiveFov = 4
let currentFov = 4 let currentFov = 4
const initialCameraUp = this.camera.up.clone() const initialCameraUp = this.camera.up.clone()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
const tempVec = new Vector3() const tempVec = new Vector3()
@ -1031,7 +999,6 @@ export class CameraControls {
this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t) const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
this.camera.up.copy(currentUp) this.camera.up.copy(currentUp)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.dollyZoom(currentFov) this.dollyZoom(currentFov)
} }
@ -1060,7 +1027,6 @@ export class CameraControls {
this.lastPerspectiveFov = 4 this.lastPerspectiveFov = 4
let currentFov = 4 let currentFov = 4
const initialCameraUp = this.camera.up.clone() const initialCameraUp = this.camera.up.clone()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.usePerspectiveCamera() this.usePerspectiveCamera()
const tempVec = new Vector3() const tempVec = new Vector3()
@ -1209,7 +1175,7 @@ function convertThreeCamValuesToEngineCam({
const lookAt = buildLookAt(64 / zoom, target, position) const lookAt = buildLookAt(64 / zoom, target, position)
return { return {
center: new Vector3(lookAt.center.x, lookAt.center.y, lookAt.center.z), center: new Vector3(lookAt.center.x, lookAt.center.y, lookAt.center.z),
up: new Vector3(upVector.x, upVector.y, upVector.z), up: new Vector3(0, 0, 1),
vantage: new Vector3(lookAt.eye.x, lookAt.eye.y, lookAt.eye.z), vantage: new Vector3(lookAt.eye.x, lookAt.eye.y, lookAt.eye.z),
} }
} }

View File

@ -5,7 +5,7 @@ import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra' import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls' import { ReactCameraProperties } from './CameraControls'
import { throttle, toSync } from 'lib/utils' import { throttle } from 'lib/utils'
import { import {
sceneInfra, sceneInfra,
kclManager, kclManager,
@ -34,15 +34,17 @@ import { CustomIcon, CustomIconName } from 'components/CustomIcon'
import { ConstrainInfo } from 'lang/std/stdTypes' import { ConstrainInfo } from 'lang/std/stdTypes'
import { getConstraintInfo } from 'lang/std/sketch' import { getConstraintInfo } from 'lang/std/sketch'
import { Dialog, Popover, Transition } from '@headlessui/react' import { Dialog, Popover, Transition } from '@headlessui/react'
import { LineInputsType } from 'lang/std/sketchcombos'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { InstanceProps, create } from 'react-modal-promise' import { InstanceProps, create } from 'react-modal-promise'
import { executeAst } from 'lang/langHelpers' import { executeAst } from 'lang/langHelpers'
import { import {
deleteSegmentFromPipeExpression, deleteSegmentFromPipeExpression,
makeRemoveSingleConstraintInput,
removeSingleConstraintInfo, removeSingleConstraintInfo,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { err, reportRejection, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } { function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false) const [isCamMoving, setIsCamMoving] = useState(false)
@ -122,9 +124,9 @@ export const ClientSideScene = ({
} else if (context.mouseState.type === 'isDragging') { } else if (context.mouseState.type === 'isDragging') {
cursor = 'grabbing' cursor = 'grabbing'
} else if ( } else if (
state.matches({ Sketch: 'Line tool' }) || state.matches('Sketch.Line tool') ||
state.matches({ Sketch: 'Tangential arc to' }) || state.matches('Sketch.Tangential arc to') ||
state.matches({ Sketch: 'Rectangle tool' }) state.matches('Sketch.Rectangle tool')
) { ) {
cursor = 'crosshair' cursor = 'crosshair'
} else { } else {
@ -212,9 +214,9 @@ const Overlay = ({
overlay.visible && overlay.visible &&
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' && typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' &&
!( !(
state.matches({ Sketch: 'Line tool' }) || state.matches('Sketch.Line tool') ||
state.matches({ Sketch: 'Tangential arc to' }) || state.matches('Sketch.Tangential arc to') ||
state.matches({ Sketch: 'Rectangle tool' }) state.matches('Sketch.Rectangle tool')
) )
return ( return (
@ -540,10 +542,12 @@ const ConstraintSymbol = ({
iconName: 'dimension', iconName: 'dimension',
}, },
} }
const varName = varNameMap?.[_type]?.varName || 'var' const varName =
const name: CustomIconName = varNameMap[_type].iconName _type in varNameMap ? varNameMap[_type as LineInputsType].varName : 'var'
const displayName = varNameMap[_type]?.displayName const name: CustomIconName = varNameMap[_type as LineInputsType].iconName
const implicitDesc = varNameMap[_type]?.implicitConstraintDesc const displayName = varNameMap[_type as LineInputsType]?.displayName
const implicitDesc =
varNameMap[_type as LineInputsType]?.implicitConstraintDesc
const _node = useMemo( const _node = useMemo(
() => getNodeFromPath<Expr>(kclManager.ast, pathToNode), () => getNodeFromPath<Expr>(kclManager.ast, pathToNode),
@ -578,7 +582,7 @@ const ConstraintSymbol = ({
}} }}
// disabled={isConstrained || !convertToVarEnabled} // disabled={isConstrained || !convertToVarEnabled}
// disabled={implicitDesc} TODO why does this change styles that are hard to override? // disabled={implicitDesc} TODO why does this change styles that are hard to override?
onClick={toSync(async () => { onClick={async () => {
if (!isConstrained) { if (!isConstrained) {
send({ send({
type: 'Convert to variable', type: 'Convert to variable',
@ -600,23 +604,25 @@ const ConstraintSymbol = ({
if (trap(_node1)) return Promise.reject(_node1) if (trap(_node1)) return Promise.reject(_node1)
const shallowPath = _node1.shallowPath const shallowPath = _node1.shallowPath
if (!context.sketchDetails || !argPosition) return const input = makeRemoveSingleConstraintInput(
const transform = removeSingleConstraintInfo(
shallowPath,
argPosition, argPosition,
shallowPath
)
if (!input || !context.sketchDetails) return
const transform = removeSingleConstraintInfo(
input,
kclManager.ast, kclManager.ast,
kclManager.programMemory kclManager.programMemory
) )
if (!transform) return if (!transform) return
const { modifiedAst } = transform const { modifiedAst } = transform
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.updateAst(modifiedAst, true) kclManager.updateAst(modifiedAst, true)
} catch (e) { } catch (e) {
console.log('error', e) console.log('error', e)
} }
toast.success('Constraint removed') toast.success('Constraint removed')
} }
}, reportRejection)} }}
> >
<CustomIcon name={name} /> <CustomIcon name={name} />
</button> </button>
@ -682,7 +688,7 @@ const ConstraintSymbol = ({
const throttled = throttle((a: ReactCameraProperties) => { const throttled = throttle((a: ReactCameraProperties) => {
if (a.type === 'perspective' && a.fov) { if (a.type === 'perspective' && a.fov) {
sceneInfra.camControls.dollyZoom(a.fov).catch(reportRejection) sceneInfra.camControls.dollyZoom(a.fov)
} }
}, 1000 / 15) }, 1000 / 15)
@ -712,7 +718,6 @@ export const CamDebugSettings = () => {
if (camSettings.type === 'perspective') { if (camSettings.type === 'perspective') {
sceneInfra.camControls.useOrthographicCamera() sceneInfra.camControls.useOrthographicCamera()
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneInfra.camControls.usePerspectiveCamera(true) sceneInfra.camControls.usePerspectiveCamera(true)
} }
}} }}
@ -720,7 +725,7 @@ export const CamDebugSettings = () => {
<div> <div>
<button <button
onClick={() => { onClick={() => {
sceneInfra.camControls.resetCameraPosition().catch(reportRejection) sceneInfra.camControls.resetCameraPosition()
}} }}
> >
Reset Camera Position Reset Camera Position

Some files were not shown because too many files have changed in this diff Show More