Compare commits
139 Commits
ok
...
franknoiro
Author | SHA1 | Date | |
---|---|---|---|
58b643cedd | |||
5512f99997 | |||
01cc9e751b | |||
bfac6b7dc8 | |||
d1f9a02ffa | |||
d8236dd8da | |||
dabf256e2b | |||
4285e81001 | |||
370375c328 | |||
9f22882c68 | |||
db5331d9b9 | |||
5cc92f0162 | |||
2978e80226 | |||
4a74c60150 | |||
00fa40bbc9 | |||
62b78840b6 | |||
f828c36e58 | |||
8c5b146c94 | |||
61c7d9844d | |||
8d48c17395 | |||
0ff820d4da | |||
c4ff1c2ef1 | |||
b6aba2f29c | |||
7467f7ea50 | |||
0c6d3e0ccf | |||
e82917ea01 | |||
857c1aad3d | |||
dc73acb1b1 | |||
8602e937d3 | |||
a2133d8317 | |||
39ce0da3e5 | |||
f235a950b0 | |||
3cd3e1af72 | |||
8c6266e94b | |||
755a6016c7 | |||
1cbbefba97 | |||
8610d606f4 | |||
728e87a627 | |||
772034af68 | |||
957a9ca4fe | |||
472eb2bafe | |||
88216d4c76 | |||
8b1e4d6708 | |||
769c3ec785 | |||
1c4179a9db | |||
292f89859f | |||
a00800bddc | |||
78ceba6d20 | |||
6776a350af | |||
dd75f06f77 | |||
394872d84e | |||
f9eef6397f | |||
900bac999c | |||
5b2738f826 | |||
dab96577a7 | |||
25443eba31 | |||
0a72d7a39a | |||
5f8d4f8294 | |||
7c2cfba0ac | |||
5ee43bda22 | |||
a1b6bbac7e | |||
e61516f3c3 | |||
e2eeec37ad | |||
d7fcc128aa | |||
cf266b17c1 | |||
b3a1796da9 | |||
39b9a6b2c4 | |||
6ba4fa305c | |||
1d043899c8 | |||
cb8a087d89 | |||
f2eb7b57b8 | |||
eba653930f | |||
3deb5c689a | |||
11ebe11111 | |||
9538ffb8ec | |||
55d1da226f | |||
2bfde64bf1 | |||
7cb9a2efd9 | |||
57e85d7fd0 | |||
ca4a442cce | |||
46eef39d53 | |||
dbc5f7b11f | |||
6797331c9d | |||
cc80a2da3d | |||
54fb9c903a | |||
e63597458a | |||
e15c38fa23 | |||
906ca65611 | |||
805b9f48e5 | |||
a762d741a5 | |||
4b8ca7f61f | |||
31b0a8af12 | |||
74b4cb9e08 | |||
e7c6dd3698 | |||
aa9abbe83f | |||
b19f3bbdb0 | |||
892e856471 | |||
84fae12cdd | |||
3d67781039 | |||
114c3a2580 | |||
02b4aa0476 | |||
57f4e1b79c | |||
35f9b82a65 | |||
cbddb3553d | |||
dd754c78ab | |||
150f56b47a | |||
0eef6ab7d3 | |||
91d3ba3fce | |||
7165aa1b41 | |||
3cbda10eab | |||
0f3432b5a0 | |||
f11dc07f0b | |||
e49beb6609 | |||
b8f27b77a8 | |||
fa7e31223d | |||
f04c4588df | |||
c95812efa6 | |||
96385cd5ee | |||
64707edaad | |||
27baf135e7 | |||
a4cf68c661 | |||
403e074249 | |||
50259aa052 | |||
1739f3dafe | |||
7ceb518446 | |||
36a6b8c0ea | |||
bbdca7421e | |||
03c6f6d60e | |||
18c7e7934a | |||
bf650fd129 | |||
81ccb65f15 | |||
335b5100ae | |||
1162ff3b03 | |||
5e8227ead8 | |||
ed339a6b9a | |||
1d19fc6b7e | |||
5b5355376f | |||
5c90f72c91 | |||
026a8d19cb |
@ -2,7 +2,9 @@ NODE_ENV=development
|
||||
DEV=true
|
||||
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
|
||||
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
|
||||
BASE_URL=https://api.dev.zoo.dev
|
||||
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||
VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"
|
||||
# ONLY add your token in .env.development.local if you want to skip auth, otherwise this token takes precedence!
|
||||
#VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"
|
||||
|
@ -13,6 +13,8 @@
|
||||
"plugin:css-modules/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
@ -24,7 +26,6 @@
|
||||
{
|
||||
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
||||
"rules": {
|
||||
"@typescript-eslint/no-floating-promises": "warn",
|
||||
"suggest-no-throw/suggest-no-throw": "off",
|
||||
"testing-library/prefer-screen-queries": "off",
|
||||
"jest/valid-expect": "off"
|
||||
|
357
.github/workflows/build-test-publish-apps.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: build-test-publish-apps
|
||||
name: build-publish-apps
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@ -21,7 +21,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prepare-json-files:
|
||||
prepare-files:
|
||||
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
|
||||
outputs:
|
||||
version: ${{ steps.export_version.outputs.version }}
|
||||
@ -33,98 +33,80 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
# TODO: see if we can fetch from main instead if no diff at src/wasm-lib
|
||||
- name: Run build:wasm
|
||||
run: "yarn build:wasm"
|
||||
|
||||
- name: Set nightly version
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
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 ned to add updater test URL here https://dl.zoo.dev/releases/modeling-app/updater-test/last_update.json
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
|
||||
with:
|
||||
name: prepared-files
|
||||
path: |
|
||||
package.json
|
||||
src/wasm-lib/pkg/wasm_lib*
|
||||
|
||||
- id: export_version
|
||||
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
|
||||
|
||||
|
||||
build-test-app-macos:
|
||||
needs: [prepare-json-files]
|
||||
runs-on: macos-14
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
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'
|
||||
- name: Prepare electron-builder.yml file for updater test
|
||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||
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' || ''}}"
|
||||
|
||||
# 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
|
||||
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test"' electron-builder.yml
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: "out/make/*/*/*/*"
|
||||
name: prepared-files-updater-test
|
||||
path: |
|
||||
electron-builder.yml
|
||||
|
||||
|
||||
build-test-app-windows:
|
||||
needs: [prepare-json-files]
|
||||
runs-on: windows-2022
|
||||
build-apps:
|
||||
needs: [prepare-files]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-14, windows-2022, ubuntu-22.04]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
|
||||
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
|
||||
WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
name: prepared-files
|
||||
|
||||
- name: Copy updated .json files
|
||||
if: github.event_name == 'schedule'
|
||||
- name: Copy prepared files
|
||||
run: |
|
||||
ls -l artifact
|
||||
cp artifact/package.json package.json
|
||||
ls -R prepared-files
|
||||
cp prepared-files/package.json package.json
|
||||
cp prepared-files/src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
mkdir src/wasm-lib/pkg
|
||||
cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v4
|
||||
@ -134,26 +116,10 @@ jobs:
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: Run build:wasm manually
|
||||
shell: bash
|
||||
env:
|
||||
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
|
||||
run: |
|
||||
mkdir src/wasm-lib/pkg; cd src/wasm-lib
|
||||
echo "building with ${{ env.MODE }}"
|
||||
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
|
||||
cd ../../
|
||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
- run: yarn tronb:vite
|
||||
|
||||
- name: Prepare certificate and variables (Windows only)
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }}
|
||||
run: |
|
||||
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
|
||||
cat /d/Certificate_pkcs12.p12
|
||||
@ -168,7 +134,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Setup certicate with SSM KSP (Windows only)
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'windows-2022' }}
|
||||
run: |
|
||||
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
|
||||
msiexec /i smtools-windows-x64.msi /quiet /qn
|
||||
@ -178,83 +144,42 @@ jobs:
|
||||
smksp_cert_sync.exe
|
||||
shell: cmd
|
||||
|
||||
- name: Build the app for x64
|
||||
run: "yarn electron-forge make --arch x64"
|
||||
- name: Build the app
|
||||
run: yarn electron-builder --config ${{ env.BUILD_RELEASE && '--publish always' || '' }}
|
||||
|
||||
- name: Build the app for arm64
|
||||
run: "yarn electron-forge make --arch arm64"
|
||||
|
||||
- name: List artifacts
|
||||
run: "ls -R out/make"
|
||||
|
||||
- name: Sign using Signtool
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
env:
|
||||
THUMBPRINT: "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D"
|
||||
X64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\x64\\Zoo Modeling App-*Setup.exe"
|
||||
ARM64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\arm64\\Zoo Modeling App-*Setup.exe"
|
||||
run: |
|
||||
signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.X64_FILE }}"
|
||||
signtool.exe verify /v /pa "${{ env.X64_FILE }}"
|
||||
signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.ARM64_FILE }}"
|
||||
signtool.exe verify /v /pa "${{ env.ARM64_FILE }}"
|
||||
- name: List artifacts in out/
|
||||
run: ls -R out
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: "out/make/*/*/*"
|
||||
|
||||
# TODO: Run e2e tests
|
||||
|
||||
|
||||
build-test-app-ubuntu:
|
||||
needs: [prepare-json-files]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
if: github.event_name == 'schedule'
|
||||
|
||||
- name: Copy updated .json files
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
ls -l artifact
|
||||
cp artifact/package.json package.json
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: Run build:wasm
|
||||
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
|
||||
|
||||
- name: Build the app for arm64
|
||||
run: "yarn electron-forge make --arch arm64"
|
||||
|
||||
- name: Build the app for x64
|
||||
run: "yarn electron-forge make --arch x64"
|
||||
|
||||
- name: List artifacts
|
||||
run: "ls -R out/make"
|
||||
name: out-${{ matrix.os }}
|
||||
path: |
|
||||
out/Zoo*.*
|
||||
out/latest*.yml
|
||||
|
||||
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
|
||||
|
||||
# TODO: sign the app (and updater bundle potentially)
|
||||
- uses: actions/download-artifact@v3
|
||||
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
|
||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||
with:
|
||||
path: "out/make/*/*/*"
|
||||
name: updater-test-${{ matrix.os }}
|
||||
path: |
|
||||
out/Zoo*.*
|
||||
out/latest*.yml
|
||||
|
||||
|
||||
publish-apps-release:
|
||||
@ -262,88 +187,76 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
|
||||
needs: [prepare-json-files, build-test-app-macos, build-test-app-windows, build-test-app-ubuntu]
|
||||
needs: [prepare-files, build-apps]
|
||||
env:
|
||||
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
|
||||
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
|
||||
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
|
||||
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
|
||||
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
|
||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
|
||||
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
|
||||
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
|
||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
|
||||
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
|
||||
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
|
||||
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Generate the update static endpoint
|
||||
run: |
|
||||
ls -l artifact/*/*oo*
|
||||
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
||||
WINDOWS_X86_64_SIG=`cat artifact/msi/*x64*.msi.zip.sig`
|
||||
WINDOWS_AARCH64_SIG=`cat artifact/msi/*arm64*.msi.zip.sig`
|
||||
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
|
||||
jq --null-input \
|
||||
--arg version "${VERSION}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_sig "$DARWIN_SIG" \
|
||||
--arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \
|
||||
--arg windows_x86_64_sig "$WINDOWS_X86_64_SIG" \
|
||||
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
||||
--arg windows_aarch64_sig "$WINDOWS_AARCH64_SIG" \
|
||||
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi.zip" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
"notes": $notes,
|
||||
"platforms": {
|
||||
"darwin-x86_64": {
|
||||
"signature": $darwin_sig,
|
||||
"url": $darwin_url
|
||||
},
|
||||
"darwin-aarch64": {
|
||||
"signature": $darwin_sig,
|
||||
"url": $darwin_url
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"signature": $windows_x86_64_sig,
|
||||
"url": $windows_x86_64_url
|
||||
},
|
||||
"windows-aarch64": {
|
||||
"signature": $windows_aarch64_sig,
|
||||
"url": $windows_aarch64_url
|
||||
}
|
||||
}
|
||||
}' > last_update.json
|
||||
cat last_update.json
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-windows-2022
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-macos-14
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: out-ubuntu-22.04
|
||||
path: out
|
||||
|
||||
- name: Generate the download static endpoint
|
||||
run: |
|
||||
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
|
||||
RELEASE_DIR=https://${WEBSITE_DIR}
|
||||
jq --null-input \
|
||||
--arg version "${VERSION}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \
|
||||
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
|
||||
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi" \
|
||||
--arg mac_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-mac.dmg" \
|
||||
--arg mac_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-mac.dmg" \
|
||||
--arg windows_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-win.exe" \
|
||||
--arg windows_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x64-win.exe" \
|
||||
--arg linux_arm64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-arm64-linux.AppImage" \
|
||||
--arg linux_x64_url "$RELEASE_DIR/${{ env.URL_CODED_NAME }}-${VERSION_NO_V}-x86_64-linux.AppImage" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
"notes": $notes,
|
||||
"platforms": {
|
||||
"dmg-universal": {
|
||||
"url": $darwin_url
|
||||
"dmg-arm64": {
|
||||
"url": $mac_arm64_url
|
||||
},
|
||||
"msi-x86_64": {
|
||||
"url": $windows_x86_64_url
|
||||
"dmg-x64": {
|
||||
"url": $mac_x64_url
|
||||
},
|
||||
"msi-aarch64": {
|
||||
"url": $windows_aarch64_url
|
||||
"exe-arm64": {
|
||||
"url": $windows_arm64_url
|
||||
},
|
||||
"exe-x64": {
|
||||
"url": $windows_x64_url
|
||||
},
|
||||
"appimage-arm64": {
|
||||
"url": $linux_arm64_url
|
||||
},
|
||||
"appimage-x64": {
|
||||
"url": $linux_x64_url
|
||||
}
|
||||
}
|
||||
}' > last_download.json
|
||||
cat last_download.json
|
||||
|
||||
- name: List artifacts
|
||||
run: "ls -R out"
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v2.1.5'
|
||||
with:
|
||||
@ -352,24 +265,26 @@ jobs:
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@v2.1.0
|
||||
with:
|
||||
project_id: kittycadapi
|
||||
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
|
||||
|
||||
- name: Upload release files to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
with:
|
||||
path: artifact
|
||||
glob: '*/Zoo*'
|
||||
path: out
|
||||
glob: 'Zoo*'
|
||||
parent: false
|
||||
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
||||
- name: Upload update endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
with:
|
||||
path: last_update.json
|
||||
path: out
|
||||
glob: 'latest*'
|
||||
parent: false
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
||||
- name: Upload download endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
with:
|
||||
path: last_download.json
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
@ -378,7 +293,9 @@ jobs:
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: 'artifact/*/Zoo*'
|
||||
files: 'out/Zoo*'
|
||||
|
||||
# TODO: Add GitHub publisher
|
||||
|
||||
announce_release:
|
||||
needs: [publish-apps-release]
|
||||
|
2
.github/workflows/build-test-web.yml
vendored
@ -45,7 +45,7 @@ jobs:
|
||||
- run: yarn xstate:typegen
|
||||
- run: yarn tsc
|
||||
- name: Lint
|
||||
run: yarn eslint --max-warnings 0 src e2e
|
||||
run: yarn eslint --max-warnings 0 src e2e packages/codemirror-lsp-client
|
||||
|
||||
|
||||
check-typos:
|
||||
|
3
.github/workflows/cargo-clippy.yml
vendored
@ -28,6 +28,7 @@ jobs:
|
||||
dir: ['src/wasm-lib']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/install-action@just
|
||||
- name: Install latest rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@ -41,7 +42,7 @@ jobs:
|
||||
- name: Run clippy
|
||||
run: |
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo clippy --all --tests --benches -- -D warnings
|
||||
just lint
|
||||
# If this fails, run "cargo check" to update Cargo.lock,
|
||||
# then add Cargo.lock to the PR.
|
||||
- name: Check Cargo.lock doesn't need updating
|
||||
|
20
.github/workflows/playwright.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
- 'src/wasm-lib/**'
|
||||
|
||||
playwright-chrome:
|
||||
timeout-minutes: ${{ matrix.os == 'macos-14' && 60 || 40 }}
|
||||
timeout-minutes: ${{ matrix.os == 'macos-14' && 60 || 50 }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -139,7 +139,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: playwright-report-ubuntu-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
@ -174,14 +174,14 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: steps.git-check.outputs.modified == 'true'
|
||||
with:
|
||||
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
- uses: actions/download-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
- name: Run playwright/chrome flow (with retries)
|
||||
id: retry
|
||||
@ -232,6 +232,7 @@ jobs:
|
||||
exit 0
|
||||
env:
|
||||
CI: true
|
||||
FAIL_ON_CONSOLE_ERRORS: true
|
||||
NODE_ENV: development
|
||||
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
VITE_KC_SKIP_AUTH: true
|
||||
@ -244,14 +245,14 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
@ -262,7 +263,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-14]
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 40
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
@ -351,7 +352,7 @@ jobs:
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-results-ubuntu-${{ github.sha }}
|
||||
name: test-results-${{ matrix.os }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
- name: Run electron tests (with retries)
|
||||
id: retry
|
||||
@ -381,7 +382,7 @@ jobs:
|
||||
echo "retried=true" >>$GITHUB_OUTPUT
|
||||
echo "run playwright with last failed tests and retry $retry"
|
||||
if [[ "$IS_UBUNTU" == "true" ]]; then
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --last-failed --grep=@electron || true
|
||||
else
|
||||
yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
|
||||
fi
|
||||
@ -410,6 +411,7 @@ jobs:
|
||||
exit 0
|
||||
env:
|
||||
CI: true
|
||||
FAIL_ON_CONSOLE_ERRORS: true
|
||||
NODE_ENV: development
|
||||
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
VITE_KC_SKIP_AUTH: true
|
||||
|
1
.gitignore
vendored
@ -59,6 +59,7 @@ src/wasm-lib/grackle/stdlib_cube_partial.json
|
||||
Mac_App_Distribution.provisionprofile
|
||||
|
||||
*.tsbuildinfo
|
||||
src/wasm-lib/pkg
|
||||
|
||||
venv
|
||||
.vite/
|
||||
|
8
Makefile
@ -7,6 +7,14 @@ XSTATE_TYPEGENS := $(wildcard src/machines/*.typegen.ts)
|
||||
dev: node_modules public/wasm_lib_bg.wasm $(XSTATE_TYPEGENS)
|
||||
yarn start
|
||||
|
||||
# I'm sorry this is so specific to my setup you may as well ignore this.
|
||||
# This is so you don't have to deal with electron windows popping up constantly.
|
||||
# It should work for you other Linux users.
|
||||
lee-electron-test:
|
||||
Xephyr -br -ac -noreset -screen 1200x500 :2 &
|
||||
DISPLAY=:2 NODE_ENV=development PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn tron:test -g "when using the file tree"
|
||||
killall Xephyr
|
||||
|
||||
$(XSTATE_TYPEGENS): $(TS_SRC)
|
||||
yarn xstate typegen 'src/**/*.ts?(x)'
|
||||
|
||||
|
30
README.md
@ -351,36 +351,6 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
|
||||
|
||||
</details>
|
||||
|
||||
### Tauri e2e tests
|
||||
|
||||
#### Windows (local only until the CI edge version mismatch is fixed)
|
||||
|
||||
```
|
||||
yarn install
|
||||
yarn build:wasm-dev
|
||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
yarn vite build --mode development
|
||||
yarn tauri build --debug -b
|
||||
$env:KITTYCAD_API_TOKEN="<YOUR_KITTYCAD_API_TOKEN>"
|
||||
$env:VITE_KC_API_BASE_URL="https://api.dev.zoo.dev"
|
||||
$env:E2E_TAURI_ENABLED="true"
|
||||
$env:TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}'
|
||||
$env:E2E_APPLICATION=".\src-tauri\target\debug\Zoo Modeling App.exe"
|
||||
Stop-Process -Name msedgedriver
|
||||
yarn wdio run wdio.conf.ts
|
||||
```
|
||||
|
||||
## KCL
|
||||
|
||||
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).
|
||||
|
||||
|
||||
- Theme removed as a project setting
|
||||
- Rename kcl Value to Expr, MemoryItem to KclValue
|
||||
- Remove ProgramReturn
|
||||
- Macro to make KCL snapshot tests easier
|
||||
- Add logical not operator using bang !
|
||||
- ensure we never execute over ourselves
|
||||
- Unify KCL expression execution (2 + draw() didn't work)
|
||||
- Text-CAD-integration
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# From https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof
|
||||
|
||||
KEY_CHAIN=build.keychain
|
||||
CERTIFICATE_P12=certificate.p12
|
||||
|
||||
# Recreate the certificate from the secure environment variable
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > $CERTIFICATE_P12
|
||||
|
||||
#create a keychain
|
||||
security create-keychain -p actions $KEY_CHAIN
|
||||
|
||||
# Make the keychain the default so identities are found
|
||||
security default-keychain -s $KEY_CHAIN
|
||||
|
||||
# Unlock the keychain
|
||||
security unlock-keychain -p actions $KEY_CHAIN
|
||||
|
||||
security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign;
|
||||
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
|
||||
|
||||
# remove certs
|
||||
rm -fr *.p12
|
@ -22,8 +22,3 @@ once fixed in engine will just start working here with no language changes.
|
||||
|
||||
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
|
||||
chamfer cases work currently.
|
||||
|
||||
Sketching on the chamfered face does not currently work.
|
||||
|
||||
- **Shell**: Shell sometimes does not work when arcs or fillets are involved.
|
||||
We are tracking the engine side bug on this.
|
||||
|
858
docs/kcl/arrayReduce.md
Normal file
@ -19,6 +19,7 @@ layout: manual
|
||||
* [`angledLineToX`](kcl/angledLineToX)
|
||||
* [`angledLineToY`](kcl/angledLineToY)
|
||||
* [`arc`](kcl/arc)
|
||||
* [`arrayReduce`](kcl/arrayReduce)
|
||||
* [`asin`](kcl/asin)
|
||||
* [`assert`](kcl/assert)
|
||||
* [`assertEqual`](kcl/assertEqual)
|
||||
@ -56,6 +57,7 @@ layout: manual
|
||||
* [`line`](kcl/line)
|
||||
* [`lineTo`](kcl/lineTo)
|
||||
* [`ln`](kcl/ln)
|
||||
* [`loft`](kcl/loft)
|
||||
* [`log`](kcl/log)
|
||||
* [`log10`](kcl/log10)
|
||||
* [`log2`](kcl/log2)
|
||||
@ -63,6 +65,7 @@ layout: manual
|
||||
* [`max`](kcl/max)
|
||||
* [`min`](kcl/min)
|
||||
* [`mm`](kcl/mm)
|
||||
* [`offsetPlane`](kcl/offsetPlane)
|
||||
* [`patternCircular2d`](kcl/patternCircular2d)
|
||||
* [`patternCircular3d`](kcl/patternCircular3d)
|
||||
* [`patternLinear2d`](kcl/patternLinear2d)
|
||||
@ -88,6 +91,7 @@ layout: manual
|
||||
* [`tan`](kcl/tan)
|
||||
* [`tangentialArc`](kcl/tangentialArc)
|
||||
* [`tangentialArcTo`](kcl/tangentialArcTo)
|
||||
* [`tangentialArcToRelative`](kcl/tangentialArcToRelative)
|
||||
* [`tau`](kcl/tau)
|
||||
* [`toDegrees`](kcl/toDegrees)
|
||||
* [`toRadians`](kcl/toRadians)
|
||||
|
516
docs/kcl/loft.md
Normal file
138
docs/kcl/offsetPlane.md
Normal file
29802
docs/kcl/std.json
@ -37,8 +37,7 @@ const example = extrude(10, exampleSketch)
|
||||
offset: number,
|
||||
// Radius of the arc. Not to be confused with Raiders of the Lost Ark.
|
||||
radius: number,
|
||||
} |
|
||||
[number, number]
|
||||
}
|
||||
```
|
||||
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. (REQUIRED)
|
||||
```js
|
||||
|
863
docs/kcl/tangentialArcToRelative.md
Normal file
@ -13,14 +13,16 @@ arrays can hold objects and vice versa.
|
||||
|
||||
`true` or `false` work when defining values.
|
||||
|
||||
## Variable declaration
|
||||
## Constant declaration
|
||||
|
||||
Variables are defined with the `let` keyword like so:
|
||||
Constants are defined with the `let` keyword like so:
|
||||
|
||||
```
|
||||
let myBool = false
|
||||
```
|
||||
|
||||
Currently you cannot redeclare a constant.
|
||||
|
||||
## Array
|
||||
|
||||
An array is defined with `[]` braces. What is inside the brackets can
|
||||
|
@ -8,8 +8,8 @@ import {
|
||||
PERSIST_MODELING_CONTEXT,
|
||||
} from './test-utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
@ -96,33 +96,49 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
||||
}
|
||||
|
||||
// deselect line tool
|
||||
await page.getByTestId('line').click()
|
||||
const btnLine = page.getByTestId('line')
|
||||
const btnLineAriaPressed = await btnLine.getAttribute('aria-pressed')
|
||||
if (btnLineAriaPressed === 'true') {
|
||||
await btnLine.click()
|
||||
}
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
|
||||
if (openPanes.includes('code')) {
|
||||
await expect
|
||||
.poll(async () => u.getGreatestPixDiff(line1, TEST_COLORS.WHITE))
|
||||
.toBeLessThan(3)
|
||||
await page.waitForTimeout(100)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(line1, [249, 249, 249]))
|
||||
.poll(async () => u.getGreatestPixDiff(line1, [249, 249, 249]))
|
||||
.toBeLessThan(3)
|
||||
await page.waitForTimeout(100)
|
||||
}
|
||||
|
||||
// click between first two clicks to get center of the line
|
||||
await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
if (openPanes.includes('code')) {
|
||||
expect(await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)).toBeLessThan(3)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(line1, TEST_COLORS.BLUE)
|
||||
).toBeLessThan(3)
|
||||
await expect(await u.getGreatestPixDiff(line1, [0, 0, 255])).toBeLessThan(3)
|
||||
}
|
||||
|
||||
// hold down shift
|
||||
await page.keyboard.down('Shift')
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// click between the latest two clicks to get center of the line
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// selected two lines therefore there should be two cursors
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||
await page.waitForTimeout(100)
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Length: open menu' }).click()
|
||||
|
@ -3,8 +3,8 @@ import { getUtils, setup, tearDown } from './test-utils'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
|
@ -12,8 +12,8 @@ import { bracket } from 'lib/exampleKcl'
|
||||
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates'
|
||||
import fsp from 'fs/promises'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
@ -27,9 +27,19 @@ test.describe('Code pane and errors', () => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Load the app with the working starter code
|
||||
await page.addInitScript((code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
}, bracket)
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`// Extruded Triangle
|
||||
const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([10, 0], %)
|
||||
|> line([-5, 10], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(5, sketch001)`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
@ -55,6 +65,8 @@ test.describe('Code pane and errors', () => {
|
||||
test('Opening and closing the code pane will consistently show error diagnostics', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('http://localhost:3000')
|
||||
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Load the app with the working starter code
|
||||
@ -80,7 +92,7 @@ test.describe('Code pane and errors', () => {
|
||||
|
||||
// Delete a character to break the KCL
|
||||
await u.openKclCodePanel()
|
||||
await page.getByText('extrude(').click()
|
||||
await page.getByText('thickness, bracketLeg1Sketch)').click()
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
// Ensure that a badge appears on the button
|
||||
@ -91,7 +103,7 @@ test.describe('Code pane and errors', () => {
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(page.getByText('Unexpected token: |').first()).toBeVisible()
|
||||
await expect(page.locator('.cm-tooltip').first()).toBeVisible()
|
||||
|
||||
// Close the code pane
|
||||
await codePaneButton.click()
|
||||
@ -114,7 +126,7 @@ test.describe('Code pane and errors', () => {
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(page.getByText('Unexpected token: |').first()).toBeVisible()
|
||||
await expect(page.locator('.cm-tooltip').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('When error is not in view you can click the badge to scroll to it', async ({
|
||||
@ -261,10 +273,7 @@ test(
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await u.waitForPageLoad()
|
||||
})
|
||||
|
||||
// If they're open by default, we're not actually testing anything.
|
||||
@ -292,16 +301,7 @@ test(
|
||||
|
||||
await page.getByText('router-template-slate').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await u.waitForPageLoad()
|
||||
})
|
||||
|
||||
await test.step('All panes opened before should be visible', async () => {
|
||||
|
@ -3,8 +3,8 @@ import { test, expect } from '@playwright/test'
|
||||
import { getUtils, setup, tearDown } from './test-utils'
|
||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { getUtils, setup, tearDown } from './test-utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
|
@ -43,12 +43,6 @@ test(
|
||||
// open the project
|
||||
await page.getByText(`bracket`).click()
|
||||
|
||||
// wait for the project to load
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// expect zero errors in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
@ -56,6 +50,12 @@ test(
|
||||
const exportButton = page.getByTestId('export-pane-button')
|
||||
await expect(exportButton).toBeVisible()
|
||||
|
||||
// Wait for the model to finish loading
|
||||
const modelStateIndicator = page.getByTestId(
|
||||
'model-state-indicator-execution-done'
|
||||
)
|
||||
await expect(modelStateIndicator).toBeVisible({ timeout: 60000 })
|
||||
|
||||
const gltfOption = page.getByText('glTF')
|
||||
const submitButton = page.getByText('Confirm Export')
|
||||
const exportingToastMessage = page.getByText(`Exporting...`)
|
||||
@ -104,7 +104,7 @@ test(
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBe(477327)
|
||||
.toBe(482669)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
|
@ -2,8 +2,8 @@ import { test, expect } from '@playwright/test'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { getUtils, setup, tearDown } from './test-utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
|
869
e2e/playwright/file-tree.spec.ts
Normal file
@ -0,0 +1,869 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import * as fsp from 'fs/promises'
|
||||
import * as fs from 'fs'
|
||||
import {
|
||||
executorInputPath,
|
||||
getUtils,
|
||||
setup,
|
||||
setupElectron,
|
||||
tearDown,
|
||||
} from './test-utils'
|
||||
import { join } from 'path'
|
||||
import { FILE_EXT } from 'lib/constants'
|
||||
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
})
|
||||
|
||||
test.describe('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()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Renaming in the file tree', () => {
|
||||
test(
|
||||
'A file you have open',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page, dir } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const checkUnRenamedFS = () => {
|
||||
const filePath = join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
return fs.existsSync(filePath)
|
||||
}
|
||||
const newFileName = 'newFileName'
|
||||
const checkRenamedFS = () => {
|
||||
const filePath = join(dir, 'Test Project', `${newFileName}.kcl`)
|
||||
return fs.existsSync(filePath)
|
||||
}
|
||||
|
||||
const fileToRename = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
|
||||
const renamedFile = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'newFileName.kcl' }) })
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const renameInput = page.getByPlaceholder('fileToRename.kcl')
|
||||
const codeLocator = page.locator('.cm-content')
|
||||
|
||||
await test.step('Open project and file pane', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(fileToRename).toBeVisible()
|
||||
expect(checkUnRenamedFS()).toBeTruthy()
|
||||
expect(checkRenamedFS()).toBeFalsy()
|
||||
await fileToRename.click()
|
||||
await expect(projectMenuButton).toContainText('fileToRename.kcl')
|
||||
await u.openKclCodePanel()
|
||||
await expect(codeLocator).toContainText('circle(')
|
||||
await u.closeKclCodePanel()
|
||||
})
|
||||
|
||||
await test.step('Rename the file', async () => {
|
||||
await fileToRename.click({ button: 'right' })
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFileName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the file is renamed', async () => {
|
||||
await expect(fileToRename).not.toBeAttached()
|
||||
await expect(renamedFile).toBeVisible()
|
||||
expect(checkUnRenamedFS()).toBeFalsy()
|
||||
expect(checkRenamedFS()).toBeTruthy()
|
||||
})
|
||||
|
||||
await test.step('Verify we navigated', async () => {
|
||||
await expect(projectMenuButton).toContainText(newFileName + FILE_EXT)
|
||||
const url = page.url()
|
||||
expect(url).toContain(newFileName)
|
||||
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
|
||||
await expect(projectMenuButton).not.toContainText('main.kcl')
|
||||
expect(url).not.toContain('fileToRename.kcl')
|
||||
expect(url).not.toContain('main.kcl')
|
||||
|
||||
await u.openKclCodePanel()
|
||||
await expect(codeLocator).toContainText('circle(')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'A file you do not have open',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page, dir } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const newFileName = 'newFileName'
|
||||
const checkUnRenamedFS = () => {
|
||||
const filePath = join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
return fs.existsSync(filePath)
|
||||
}
|
||||
const checkRenamedFS = () => {
|
||||
const filePath = join(dir, 'Test Project', `${newFileName}.kcl`)
|
||||
return fs.existsSync(filePath)
|
||||
}
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const fileToRename = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
|
||||
const renamedFile = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: newFileName + FILE_EXT }),
|
||||
})
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const renameInput = page.getByPlaceholder('fileToRename.kcl')
|
||||
const codeLocator = page.locator('.cm-content')
|
||||
|
||||
await test.step('Open project and file pane', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(fileToRename).toBeVisible()
|
||||
expect(checkUnRenamedFS()).toBeTruthy()
|
||||
expect(checkRenamedFS()).toBeFalsy()
|
||||
})
|
||||
|
||||
await test.step('Rename the file', async () => {
|
||||
await fileToRename.click({ button: 'right' })
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFileName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the file is renamed', async () => {
|
||||
await expect(fileToRename).not.toBeAttached()
|
||||
await expect(renamedFile).toBeVisible()
|
||||
expect(checkUnRenamedFS()).toBeFalsy()
|
||||
expect(checkRenamedFS()).toBeTruthy()
|
||||
})
|
||||
|
||||
await test.step('Verify we have not navigated', async () => {
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await expect(projectMenuButton).not.toContainText(
|
||||
newFileName + FILE_EXT
|
||||
)
|
||||
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
|
||||
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain(newFileName)
|
||||
expect(url).not.toContain('fileToRename.kcl')
|
||||
|
||||
await u.openKclCodePanel()
|
||||
await expect(codeLocator).toContainText('fillet(')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`A folder you're not inside`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page, dir } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
|
||||
recursive: true,
|
||||
})
|
||||
await fsp.copyFile(
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const folderToRename = page.getByRole('button', {
|
||||
name: 'folderToRename',
|
||||
})
|
||||
const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const originalFolderName = 'folderToRename'
|
||||
const renameInput = page.getByPlaceholder(originalFolderName)
|
||||
const newFolderName = 'newFolderName'
|
||||
const checkUnRenamedFolderFS = () => {
|
||||
const folderPath = join(dir, 'Test Project', originalFolderName)
|
||||
return fs.existsSync(folderPath)
|
||||
}
|
||||
const checkRenamedFolderFS = () => {
|
||||
const folderPath = join(dir, 'Test Project', newFolderName)
|
||||
return fs.existsSync(folderPath)
|
||||
}
|
||||
|
||||
await test.step('Open project and file pane', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain('folderToRename')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(folderToRename).toBeVisible()
|
||||
expect(checkUnRenamedFolderFS()).toBeTruthy()
|
||||
expect(checkRenamedFolderFS()).toBeFalsy()
|
||||
})
|
||||
|
||||
await test.step('Rename the folder', async () => {
|
||||
await folderToRename.click({ button: 'right' })
|
||||
await expect(renameMenuItem).toBeVisible()
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFolderName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the folder is renamed, and no navigation occurred', async () => {
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain('folderToRename')
|
||||
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await expect(renamedFolder).toBeVisible()
|
||||
await expect(folderToRename).not.toBeAttached()
|
||||
expect(checkUnRenamedFolderFS()).toBeFalsy()
|
||||
expect(checkRenamedFolderFS()).toBeTruthy()
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`A folder you are inside`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page, dir } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
|
||||
recursive: true,
|
||||
})
|
||||
await fsp.copyFile(
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const folderToRename = page.getByRole('button', {
|
||||
name: 'folderToRename',
|
||||
})
|
||||
const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
|
||||
const fileWithinFolder = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
|
||||
})
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const originalFolderName = 'folderToRename'
|
||||
const renameInput = page.getByPlaceholder(originalFolderName)
|
||||
const newFolderName = 'newFolderName'
|
||||
const checkUnRenamedFolderFS = () => {
|
||||
const folderPath = join(dir, 'Test Project', originalFolderName)
|
||||
return fs.existsSync(folderPath)
|
||||
}
|
||||
const checkRenamedFolderFS = () => {
|
||||
const folderPath = join(dir, 'Test Project', newFolderName)
|
||||
return fs.existsSync(folderPath)
|
||||
}
|
||||
|
||||
await test.step('Open project and navigate into folder', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain('folderToRename')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(folderToRename).toBeVisible()
|
||||
await folderToRename.click()
|
||||
await expect(fileWithinFolder).toBeVisible()
|
||||
await fileWithinFolder.click()
|
||||
|
||||
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
|
||||
const newUrl = page.url()
|
||||
expect(newUrl).toContain('folderToRename')
|
||||
expect(newUrl).toContain('someFileWithin.kcl')
|
||||
expect(newUrl).not.toContain('main.kcl')
|
||||
expect(checkUnRenamedFolderFS()).toBeTruthy()
|
||||
expect(checkRenamedFolderFS()).toBeFalsy()
|
||||
})
|
||||
|
||||
await test.step('Rename the folder', async () => {
|
||||
await page.waitForTimeout(60000)
|
||||
await folderToRename.click({ button: 'right' })
|
||||
await expect(renameMenuItem).toBeVisible()
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFolderName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the folder is renamed, and navigated to new path', async () => {
|
||||
const urlSnippet = encodeURIComponent(
|
||||
join(newFolderName, 'someFileWithin.kcl')
|
||||
)
|
||||
await page.waitForURL(new RegExp(urlSnippet))
|
||||
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
|
||||
await expect(renamedFolder).toBeVisible()
|
||||
await expect(folderToRename).not.toBeAttached()
|
||||
|
||||
// URL is synchronous, so we check the other stuff first
|
||||
const url = page.url()
|
||||
expect(url).not.toContain('main.kcl')
|
||||
expect(url).toContain(newFolderName)
|
||||
expect(url).toContain('someFileWithin.kcl')
|
||||
expect(checkUnRenamedFolderFS()).toBeFalsy()
|
||||
expect(checkRenamedFolderFS()).toBeTruthy()
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Deleting items from the file pane', () => {
|
||||
test(
|
||||
`delete file when main.kcl exists, navigate to main.kcl`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
const testDir = join(dir, 'testProject')
|
||||
await fsp.mkdir(testDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(testDir, 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(testDir, 'fileToDelete.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectCard = page.getByText('testProject')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const fileToDelete = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'fileToDelete.kcl' }) })
|
||||
const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
|
||||
const deleteConfirmation = page.getByTestId('delete-confirmation')
|
||||
|
||||
await test.step('Open project and navigate to fileToDelete.kcl', async () => {
|
||||
await projectCard.click()
|
||||
await u.waitForPageLoad()
|
||||
await u.openFilePanel()
|
||||
|
||||
await fileToDelete.click()
|
||||
await u.waitForPageLoad()
|
||||
await u.openKclCodePanel()
|
||||
await expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
|
||||
await u.closeKclCodePanel()
|
||||
})
|
||||
|
||||
await test.step('Delete fileToDelete.kcl', async () => {
|
||||
await fileToDelete.click({ button: 'right' })
|
||||
await expect(deleteMenuItem).toBeVisible()
|
||||
await deleteMenuItem.click()
|
||||
await expect(deleteConfirmation).toBeVisible()
|
||||
await deleteConfirmation.click()
|
||||
})
|
||||
|
||||
await test.step('Check deletion and navigation', async () => {
|
||||
await u.waitForPageLoad()
|
||||
await expect(fileToDelete).not.toBeVisible()
|
||||
await u.closeFilePanel()
|
||||
await u.openKclCodePanel()
|
||||
await expect(u.codeLocator).toContainText('circle(')
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test.fixme(
|
||||
'TODO - delete file we have open when main.kcl does not exist',
|
||||
async () => {}
|
||||
)
|
||||
|
||||
test(
|
||||
`Delete folder we are not in, don't navigate`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.mkdir(join(dir, 'Test Project', 'folderToDelete'), {
|
||||
recursive: true,
|
||||
})
|
||||
await fsp.copyFile(
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'folderToDelete', 'someFileWithin.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectCard = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const folderToDelete = page.getByRole('button', {
|
||||
name: 'folderToDelete',
|
||||
})
|
||||
const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
|
||||
const deleteConfirmation = page.getByTestId('delete-confirmation')
|
||||
|
||||
await test.step('Open project and open project pane', async () => {
|
||||
await projectCard.click()
|
||||
await u.waitForPageLoad()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await u.closeKclCodePanel()
|
||||
await u.openFilePanel()
|
||||
})
|
||||
|
||||
await test.step('Delete folderToDelete', async () => {
|
||||
await folderToDelete.click({ button: 'right' })
|
||||
await expect(deleteMenuItem).toBeVisible()
|
||||
await deleteMenuItem.click()
|
||||
await expect(deleteConfirmation).toBeVisible()
|
||||
await deleteConfirmation.click()
|
||||
})
|
||||
|
||||
await test.step('Check deletion and no navigation', async () => {
|
||||
await expect(folderToDelete).not.toBeAttached()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`Delete folder we are in, navigate to main.kcl`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.mkdir(join(dir, 'Test Project', 'folderToDelete'), {
|
||||
recursive: true,
|
||||
})
|
||||
await fsp.copyFile(
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'folderToDelete', 'someFileWithin.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectCard = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const folderToDelete = page.getByRole('button', {
|
||||
name: 'folderToDelete',
|
||||
})
|
||||
const fileWithinFolder = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
|
||||
})
|
||||
const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
|
||||
const deleteConfirmation = page.getByTestId('delete-confirmation')
|
||||
|
||||
await test.step('Open project and navigate into folderToDelete', async () => {
|
||||
await projectCard.click()
|
||||
await u.waitForPageLoad()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await u.closeKclCodePanel()
|
||||
await u.openFilePanel()
|
||||
|
||||
await folderToDelete.click()
|
||||
await expect(fileWithinFolder).toBeVisible()
|
||||
await fileWithinFolder.click()
|
||||
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
|
||||
})
|
||||
|
||||
await test.step('Delete folderToDelete', async () => {
|
||||
await folderToDelete.click({ button: 'right' })
|
||||
await expect(deleteMenuItem).toBeVisible()
|
||||
await deleteMenuItem.click()
|
||||
await expect(deleteConfirmation).toBeVisible()
|
||||
await deleteConfirmation.click()
|
||||
})
|
||||
|
||||
await test.step('Check deletion and navigation to main.kcl', async () => {
|
||||
await expect(folderToDelete).not.toBeAttached()
|
||||
await expect(fileWithinFolder).not.toBeAttached()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test.fixme('TODO - delete folder we are in, with no main.kcl', async () => {})
|
||||
})
|
270
e2e/playwright/lib/console-error-whitelist.ts
Normal file
@ -0,0 +1,270 @@
|
||||
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
|
||||
}
|
@ -38,7 +38,7 @@ test(
|
||||
await expect(page.getByText(notFoundText).first()).not.toBeVisible()
|
||||
|
||||
// Find the make button
|
||||
const makeButton = page.getByRole('button', { name: 'Make' })
|
||||
const makeButton = page.getByRole('button', { name: 'Make part' })
|
||||
// Make sure the button is visible but disabled
|
||||
await expect(makeButton).toBeVisible()
|
||||
await expect(makeButton).toBeDisabled()
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
import fsp from 'fs/promises'
|
||||
import fs from 'fs'
|
||||
import { join } from 'path'
|
||||
import { FILE_EXT } from 'lib/constants'
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
@ -147,9 +146,6 @@ test.describe('Can export from electron app', () => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
page.on('console', console.log)
|
||||
await electronApp.context().addInitScript(async () => {
|
||||
;(window as any).playwrightSkipFilePicker = true
|
||||
})
|
||||
|
||||
const pointOnModel = { x: 630, y: 280 }
|
||||
|
||||
@ -173,10 +169,10 @@ test.describe('Can export from electron app', () => {
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
const exportLocations: Array<Paths> = []
|
||||
@ -207,7 +203,7 @@ test.describe('Can export from electron app', () => {
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBe(477327)
|
||||
.toBe(482669)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
@ -495,10 +491,6 @@ test(
|
||||
|
||||
await file.click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await expect(u.codeLocator).toContainText(
|
||||
'A mounting bracket for the Focusrite Scarlett Solo audio interface'
|
||||
)
|
||||
@ -856,10 +848,10 @@ const extrude001 = extrude(200, sketch001)`)
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
.toBeLessThan(15)
|
||||
|
||||
await expect(async () => {
|
||||
await page.mouse.move(0, 0, { steps: 5 })
|
||||
@ -867,8 +859,8 @@ const extrude001 = extrude(200, sketch001)`)
|
||||
await page.mouse.click(pointOnModel.x, pointOnModel.y)
|
||||
// check user can interact with model by checking it turns yellow
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [176, 180, 132]))
|
||||
.toBeLessThan(10)
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [180, 180, 137]))
|
||||
.toBeLessThan(15)
|
||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||
|
||||
await page.getByTestId('app-logo').click()
|
||||
@ -942,24 +934,15 @@ test(
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await u.waitForPageLoad()
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
|
||||
@ -976,24 +959,15 @@ test(
|
||||
|
||||
await page.getByText('router-template-slate').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
await u.waitForPageLoad()
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [143, 143, 143]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await test.step('Opening the router-template project should load the stream', async () => {
|
||||
@ -1396,455 +1370,6 @@ test(
|
||||
}
|
||||
)
|
||||
|
||||
test.describe('Renaming in the file tree', () => {
|
||||
test(
|
||||
'A file you have open',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page, dir } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const checkUnRenamedFS = () => {
|
||||
const filePath = join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
return fs.existsSync(filePath)
|
||||
}
|
||||
const newFileName = 'newFileName'
|
||||
const checkRenamedFS = () => {
|
||||
const filePath = join(dir, 'Test Project', `${newFileName}.kcl`)
|
||||
return fs.existsSync(filePath)
|
||||
}
|
||||
|
||||
const fileToRename = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
|
||||
const renamedFile = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'newFileName.kcl' }) })
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const renameInput = page.getByPlaceholder('fileToRename.kcl')
|
||||
const codeLocator = page.locator('.cm-content')
|
||||
|
||||
await test.step('Open project and file pane', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(fileToRename).toBeVisible()
|
||||
expect(checkUnRenamedFS()).toBeTruthy()
|
||||
expect(checkRenamedFS()).toBeFalsy()
|
||||
await fileToRename.click()
|
||||
await expect(projectMenuButton).toContainText('fileToRename.kcl')
|
||||
await u.openKclCodePanel()
|
||||
await expect(codeLocator).toContainText('circle(')
|
||||
await u.closeKclCodePanel()
|
||||
})
|
||||
|
||||
await test.step('Rename the file', async () => {
|
||||
await fileToRename.click({ button: 'right' })
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFileName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the file is renamed', async () => {
|
||||
await expect(fileToRename).not.toBeAttached()
|
||||
await expect(renamedFile).toBeVisible()
|
||||
expect(checkUnRenamedFS()).toBeFalsy()
|
||||
expect(checkRenamedFS()).toBeTruthy()
|
||||
})
|
||||
|
||||
await test.step('Verify we navigated', async () => {
|
||||
await expect(projectMenuButton).toContainText(newFileName + FILE_EXT)
|
||||
const url = page.url()
|
||||
expect(url).toContain(newFileName)
|
||||
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
|
||||
await expect(projectMenuButton).not.toContainText('main.kcl')
|
||||
expect(url).not.toContain('fileToRename.kcl')
|
||||
expect(url).not.toContain('main.kcl')
|
||||
|
||||
await u.openKclCodePanel()
|
||||
await expect(codeLocator).toContainText('circle(')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'A file you do not have open',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page, dir } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const newFileName = 'newFileName'
|
||||
const checkUnRenamedFS = () => {
|
||||
const filePath = join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
return fs.existsSync(filePath)
|
||||
}
|
||||
const checkRenamedFS = () => {
|
||||
const filePath = join(dir, 'Test Project', `${newFileName}.kcl`)
|
||||
return fs.existsSync(filePath)
|
||||
}
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const fileToRename = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
|
||||
const renamedFile = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: newFileName + FILE_EXT }),
|
||||
})
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const renameInput = page.getByPlaceholder('fileToRename.kcl')
|
||||
const codeLocator = page.locator('.cm-content')
|
||||
|
||||
await test.step('Open project and file pane', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(fileToRename).toBeVisible()
|
||||
expect(checkUnRenamedFS()).toBeTruthy()
|
||||
expect(checkRenamedFS()).toBeFalsy()
|
||||
})
|
||||
|
||||
await test.step('Rename the file', async () => {
|
||||
await fileToRename.click({ button: 'right' })
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFileName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the file is renamed', async () => {
|
||||
await expect(fileToRename).not.toBeAttached()
|
||||
await expect(renamedFile).toBeVisible()
|
||||
expect(checkUnRenamedFS()).toBeFalsy()
|
||||
expect(checkRenamedFS()).toBeTruthy()
|
||||
})
|
||||
|
||||
await test.step('Verify we have not navigated', async () => {
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await expect(projectMenuButton).not.toContainText(
|
||||
newFileName + FILE_EXT
|
||||
)
|
||||
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
|
||||
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain(newFileName)
|
||||
expect(url).not.toContain('fileToRename.kcl')
|
||||
|
||||
await u.openKclCodePanel()
|
||||
await expect(codeLocator).toContainText('fillet(')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`A folder you're not inside`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page, dir } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
|
||||
recursive: true,
|
||||
})
|
||||
await fsp.copyFile(
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const folderToRename = page.getByRole('button', {
|
||||
name: 'folderToRename',
|
||||
})
|
||||
const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const originalFolderName = 'folderToRename'
|
||||
const renameInput = page.getByPlaceholder(originalFolderName)
|
||||
const newFolderName = 'newFolderName'
|
||||
const checkUnRenamedFolderFS = () => {
|
||||
const folderPath = join(dir, 'Test Project', originalFolderName)
|
||||
return fs.existsSync(folderPath)
|
||||
}
|
||||
const checkRenamedFolderFS = () => {
|
||||
const folderPath = join(dir, 'Test Project', newFolderName)
|
||||
return fs.existsSync(folderPath)
|
||||
}
|
||||
|
||||
await test.step('Open project and file pane', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain('folderToRename')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(folderToRename).toBeVisible()
|
||||
expect(checkUnRenamedFolderFS()).toBeTruthy()
|
||||
expect(checkRenamedFolderFS()).toBeFalsy()
|
||||
})
|
||||
|
||||
await test.step('Rename the folder', async () => {
|
||||
await folderToRename.click({ button: 'right' })
|
||||
await expect(renameMenuItem).toBeVisible()
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFolderName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the folder is renamed, and no navigation occurred', async () => {
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain('folderToRename')
|
||||
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await expect(renamedFolder).toBeVisible()
|
||||
await expect(folderToRename).not.toBeAttached()
|
||||
expect(checkUnRenamedFolderFS()).toBeFalsy()
|
||||
expect(checkRenamedFolderFS()).toBeTruthy()
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`A folder you are inside`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page, dir } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
|
||||
recursive: true,
|
||||
})
|
||||
await fsp.copyFile(
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const folderToRename = page.getByRole('button', {
|
||||
name: 'folderToRename',
|
||||
})
|
||||
const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
|
||||
const fileWithinFolder = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
|
||||
})
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const originalFolderName = 'folderToRename'
|
||||
const renameInput = page.getByPlaceholder(originalFolderName)
|
||||
const newFolderName = 'newFolderName'
|
||||
const checkUnRenamedFolderFS = () => {
|
||||
const folderPath = join(dir, 'Test Project', originalFolderName)
|
||||
return fs.existsSync(folderPath)
|
||||
}
|
||||
const checkRenamedFolderFS = () => {
|
||||
const folderPath = join(dir, 'Test Project', newFolderName)
|
||||
return fs.existsSync(folderPath)
|
||||
}
|
||||
|
||||
await test.step('Open project and navigate into folder', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain('folderToRename')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(folderToRename).toBeVisible()
|
||||
await folderToRename.click()
|
||||
await expect(fileWithinFolder).toBeVisible()
|
||||
await fileWithinFolder.click()
|
||||
|
||||
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
|
||||
const newUrl = page.url()
|
||||
expect(newUrl).toContain('folderToRename')
|
||||
expect(newUrl).toContain('someFileWithin.kcl')
|
||||
expect(newUrl).not.toContain('main.kcl')
|
||||
expect(checkUnRenamedFolderFS()).toBeTruthy()
|
||||
expect(checkRenamedFolderFS()).toBeFalsy()
|
||||
})
|
||||
|
||||
await test.step('Rename the folder', async () => {
|
||||
await page.waitForTimeout(60000)
|
||||
await folderToRename.click({ button: 'right' })
|
||||
await expect(renameMenuItem).toBeVisible()
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFolderName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the folder is renamed, and navigated to new path', async () => {
|
||||
const urlSnippet = encodeURIComponent(
|
||||
join(newFolderName, 'someFileWithin.kcl')
|
||||
)
|
||||
await page.waitForURL(new RegExp(urlSnippet))
|
||||
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
|
||||
await expect(renamedFolder).toBeVisible()
|
||||
await expect(folderToRename).not.toBeAttached()
|
||||
|
||||
// URL is synchronous, so we check the other stuff first
|
||||
const url = page.url()
|
||||
expect(url).not.toContain('main.kcl')
|
||||
expect(url).toContain(newFolderName)
|
||||
expect(url).toContain('someFileWithin.kcl')
|
||||
expect(checkUnRenamedFolderFS()).toBeFalsy()
|
||||
expect(checkRenamedFolderFS()).toBeTruthy()
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Deleting files from the file pane', () => {
|
||||
test(
|
||||
`when main.kcl exists, navigate to main.kcl`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
const testDir = join(dir, 'testProject')
|
||||
await fsp.mkdir(testDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(testDir, 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
||||
join(testDir, 'fileToDelete.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectCard = page.getByText('testProject')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const fileToDelete = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'fileToDelete.kcl' }) })
|
||||
const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
|
||||
const deleteConfirmation = page.getByTestId('delete-confirmation')
|
||||
|
||||
await test.step('Open project and navigate to fileToDelete.kcl', async () => {
|
||||
await projectCard.click()
|
||||
await u.waitForPageLoad()
|
||||
await u.openFilePanel()
|
||||
|
||||
await fileToDelete.click()
|
||||
await u.waitForPageLoad()
|
||||
await u.openKclCodePanel()
|
||||
await expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
|
||||
await u.closeKclCodePanel()
|
||||
})
|
||||
|
||||
await test.step('Delete fileToDelete.kcl', async () => {
|
||||
await fileToDelete.click({ button: 'right' })
|
||||
await expect(deleteMenuItem).toBeVisible()
|
||||
await deleteMenuItem.click()
|
||||
await expect(deleteConfirmation).toBeVisible()
|
||||
await deleteConfirmation.click()
|
||||
})
|
||||
|
||||
await test.step('Check deletion and navigation', async () => {
|
||||
await u.waitForPageLoad()
|
||||
await expect(fileToDelete).not.toBeVisible()
|
||||
await u.closeFilePanel()
|
||||
await u.openKclCodePanel()
|
||||
await expect(u.codeLocator).toContainText('circle(')
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test.fixme('TODO - when main.kcl does not exist', async () => {})
|
||||
})
|
||||
|
||||
test(
|
||||
'Original project name persist after onboarding',
|
||||
{ tag: '@electron' },
|
||||
|
@ -11,8 +11,8 @@ import {
|
||||
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
@ -54,6 +54,67 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
const crypticErrorText = `ApiError`
|
||||
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
|
||||
})
|
||||
test('user should not have to press down twice in cmdbar', async ({
|
||||
page,
|
||||
}) => {
|
||||
// because the model has `line([0,0]..` it is valid code, but the model is invalid
|
||||
// regression test for https://github.com/KittyCAD/modeling-app/issues/3251
|
||||
// Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch2 = startSketchOn("XY")
|
||||
const sketch001 = startSketchAt([-0, -0])
|
||||
|> line([0, 0], %)
|
||||
|> line([-4.84, -5.29], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
|
||||
await page.goto('/')
|
||||
await u.waitForPageLoad()
|
||||
|
||||
await test.step('Check arrow down works', async () => {
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
await page
|
||||
.getByRole('option', { name: 'floppy disk arrow Export' })
|
||||
.click()
|
||||
|
||||
// press arrow down key twice
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('ArrowDown')
|
||||
|
||||
// STL is the third option, which makes sense for two arrow downs
|
||||
await expect(page.locator('[data-headlessui-state="active"]')).toHaveText(
|
||||
'STL'
|
||||
)
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(200)
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(200)
|
||||
})
|
||||
|
||||
await test.step('Check arrow up works', async () => {
|
||||
// theme in test is dark, which is the second option, which means we can test arrow up
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
await page.getByText('The overall appearance of the').click()
|
||||
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('[data-headlessui-state="active"]')).toHaveText(
|
||||
'light'
|
||||
)
|
||||
})
|
||||
})
|
||||
test('executes on load', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
@ -285,10 +346,7 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
const exportingToastMessage = page.getByText(`Exporting...`)
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
|
||||
const errorToastMessage = page.getByText(`Error while exporting`)
|
||||
await expect(errorToastMessage).toBeVisible()
|
||||
|
||||
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
||||
await expect(engineErrorToastMessage).toBeVisible()
|
||||
@ -358,6 +416,7 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await page.addInitScript(
|
||||
async ({ code }) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
;(window as any).playwrightSkipFilePicker = true
|
||||
},
|
||||
{
|
||||
code: bracket,
|
||||
@ -393,20 +452,22 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await test.step('The second export is blocked', async () => {
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
await expect(alreadyExportingToastMessage).toBeVisible()
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
await Promise.all([
|
||||
expect(exportingToastMessage.first()).toBeVisible(),
|
||||
expect(alreadyExportingToastMessage).toBeVisible(),
|
||||
])
|
||||
})
|
||||
|
||||
await test.step('The first export still succeeds', async () => {
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
|
||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
||||
await Promise.all([
|
||||
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
|
||||
expect(errorToastMessage).not.toBeVisible(),
|
||||
expect(engineErrorToastMessage).not.toBeVisible(),
|
||||
expect(successToastMessage).toBeVisible({ timeout: 15_000 }),
|
||||
expect(alreadyExportingToastMessage).not.toBeVisible({
|
||||
timeout: 15_000,
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -419,10 +480,12 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
|
||||
// Expect it to succeed.
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
||||
await Promise.all([
|
||||
expect(exportingToastMessage).not.toBeVisible(),
|
||||
expect(errorToastMessage).not.toBeVisible(),
|
||||
expect(engineErrorToastMessage).not.toBeVisible(),
|
||||
expect(alreadyExportingToastMessage).not.toBeVisible(),
|
||||
])
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
})
|
||||
|
@ -9,8 +9,8 @@ import {
|
||||
} from './test-utils'
|
||||
import { uuidv4, roundOff } from 'lib/utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
@ -40,7 +40,7 @@ test.describe('Sketch tests', () => {
|
||||
const screwRadius = 3
|
||||
const wireRadius = 2
|
||||
const wireOffset = 0.5
|
||||
|
||||
|
||||
const screwHole = startSketchOn('XY')
|
||||
${startProfileAt1}
|
||||
|> arc({
|
||||
@ -48,7 +48,7 @@ test.describe('Sketch tests', () => {
|
||||
angle_start: 0,
|
||||
angle_end: 360
|
||||
}, %)
|
||||
|
||||
|
||||
const part001 = startSketchOn('XY')
|
||||
${startProfileAt2}
|
||||
|> xLine(width * .5, %)
|
||||
@ -57,7 +57,7 @@ test.describe('Sketch tests', () => {
|
||||
|> close(%)
|
||||
|> hole(screwHole, %)
|
||||
|> extrude(thickness, %)
|
||||
|
||||
|
||||
const part002 = startSketchOn('-XZ')
|
||||
${startProfileAt3}
|
||||
|> xLine(width / 4, %)
|
||||
@ -618,19 +618,19 @@ test.describe('Sketch tests', () => {
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await click00r(30, 0)
|
||||
codeStr += ` |> startProfileAt([1.53, 0], %)`
|
||||
codeStr += ` |> startProfileAt([2.03, 0], %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(30, 0)
|
||||
codeStr += ` |> line([1.53, 0], %)`
|
||||
codeStr += ` |> line([2.04, 0], %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(0, 30)
|
||||
codeStr += ` |> line([0, -1.53], %)`
|
||||
codeStr += ` |> line([0, -2.03], %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(-30, 0)
|
||||
codeStr += ` |> line([-1.53, 0], %)`
|
||||
codeStr += ` |> line([-2.04, 0], %)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(undefined, undefined)
|
||||
@ -954,4 +954,68 @@ const sketch002 = startSketchOn(extrude001, 'END')
|
||||
await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor)
|
||||
).toBeLessThan(3)
|
||||
})
|
||||
|
||||
test('Can attempt to sketch on revolved face', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
test.skip(
|
||||
browserName === 'webkit',
|
||||
'Skip on Safari until `window.tearDown` is working there'
|
||||
)
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const lugHeadLength = 0.25
|
||||
const lugDiameter = 0.5
|
||||
const lugLength = 2
|
||||
|
||||
fn lug = (origin, length, diameter, plane) => {
|
||||
const lugSketch = startSketchOn(plane)
|
||||
|> startProfileAt([origin[0] + lugDiameter / 2, origin[1]], %)
|
||||
|> angledLineOfYLength({ angle: 60, length: lugHeadLength }, %)
|
||||
|> xLineTo(0 + .001, %)
|
||||
|> yLineTo(0, %)
|
||||
|> close(%)
|
||||
|> revolve({ axis: "Y" }, %)
|
||||
|
||||
return lugSketch
|
||||
}
|
||||
|
||||
lug([0, 0], 10, .5, "XY")`
|
||||
)
|
||||
})
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
/***
|
||||
* Test Plan
|
||||
* Start the sketch mode
|
||||
* Click the middle of the screen which should click the top face that is revolved
|
||||
* Wait till you see the line tool be enabled
|
||||
* Wait till you see the exit sketch enabled
|
||||
*
|
||||
* This is supposed to test that you are allowed to go into sketch mode to sketch on a revolved face
|
||||
*/
|
||||
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
|
||||
await expect(async () => {
|
||||
await page.mouse.click(600, 250)
|
||||
await page.waitForTimeout(1000)
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Exit Sketch' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'line Line', exact: true })
|
||||
).toHaveAttribute('aria-pressed', 'true')
|
||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
@ -2,8 +2,8 @@ import { test, expect } from '@playwright/test'
|
||||
|
||||
import { commonPoints, getUtils, setup, tearDown } from './test-utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
|
@ -26,7 +26,9 @@ import {
|
||||
import * as TOML from '@iarna/toml'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { SETTINGS_FILE_NAME } from 'lib/constants'
|
||||
import { isErrorWhitelisted } from './lib/console-error-whitelist'
|
||||
import { isArray } from 'lib/utils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
type TestColor = [number, number, number]
|
||||
export const TEST_COLORS = {
|
||||
@ -439,46 +441,78 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
}
|
||||
return maxDiff
|
||||
},
|
||||
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
|
||||
new Promise(async (resolve) => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp1.png',
|
||||
fullPage: true,
|
||||
})
|
||||
await fn()
|
||||
const isImageDiff = async () => {
|
||||
getPixelRGBs: async (
|
||||
coords: { x: number; y: number },
|
||||
radius: number
|
||||
): Promise<[number, number, number][]> => {
|
||||
const buffer = await page.screenshot({
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot = await PNG.sync.read(buffer)
|
||||
const pixMultiplier: number = await page.evaluate(
|
||||
'window.devicePixelRatio'
|
||||
)
|
||||
const allCords: [number, number][] = [[coords.x, coords.y]]
|
||||
for (let i = 1; i < radius; i++) {
|
||||
allCords.push([coords.x + i, coords.y])
|
||||
allCords.push([coords.x - i, coords.y])
|
||||
allCords.push([coords.x, coords.y + i])
|
||||
allCords.push([coords.x, coords.y - i])
|
||||
}
|
||||
return allCords.map(([x, y]) => {
|
||||
const index =
|
||||
(screenshot.width * y * pixMultiplier + x * pixMultiplier) * 4 // rbga is 4 channels
|
||||
return [
|
||||
screenshot.data[index],
|
||||
screenshot.data[index + 1],
|
||||
screenshot.data[index + 2],
|
||||
]
|
||||
})
|
||||
},
|
||||
doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
;(async () => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp2.png',
|
||||
path: './e2e/playwright/temp1.png',
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot1 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp1.png')
|
||||
)
|
||||
const screenshot2 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp2.png')
|
||||
)
|
||||
const actualDiffCount = pixelMatch(
|
||||
screenshot1.data,
|
||||
screenshot2.data,
|
||||
null,
|
||||
screenshot1.width,
|
||||
screenshot2.height
|
||||
)
|
||||
return actualDiffCount > diffCount
|
||||
}
|
||||
|
||||
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
||||
let count = 0
|
||||
const interval = setInterval(async () => {
|
||||
count++
|
||||
if (await isImageDiff()) {
|
||||
clearInterval(interval)
|
||||
resolve(true)
|
||||
} else if (count > 100) {
|
||||
clearInterval(interval)
|
||||
resolve(false)
|
||||
await fn()
|
||||
const isImageDiff = async () => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp2.png',
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot1 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp1.png')
|
||||
)
|
||||
const screenshot2 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp2.png')
|
||||
)
|
||||
const actualDiffCount = pixelMatch(
|
||||
screenshot1.data,
|
||||
screenshot2.data,
|
||||
null,
|
||||
screenshot1.width,
|
||||
screenshot2.height
|
||||
)
|
||||
return actualDiffCount > diffCount
|
||||
}
|
||||
}, 50)
|
||||
|
||||
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
||||
let count = 0
|
||||
const interval = setInterval(() => {
|
||||
;(async () => {
|
||||
count++
|
||||
if (await isImageDiff()) {
|
||||
clearInterval(interval)
|
||||
resolve(true)
|
||||
} else if (count > 100) {
|
||||
clearInterval(interval)
|
||||
resolve(false)
|
||||
}
|
||||
})().catch(reportRejection)
|
||||
}, 50)
|
||||
})().catch(reportRejection)
|
||||
}),
|
||||
emulateNetworkConditions: async (
|
||||
networkOptions: Protocol.Network.emulateNetworkConditionsParameters
|
||||
@ -511,10 +545,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
|
||||
editorTextMatches: async (code: string) => {
|
||||
const editor = page.locator(editorSelector)
|
||||
const editorText = await editor.textContent()
|
||||
return expect(util.toNormalizedCode(editorText || '')).toBe(
|
||||
util.toNormalizedCode(code)
|
||||
)
|
||||
return expect(editor).toHaveText(code, { useInnerText: true })
|
||||
},
|
||||
|
||||
pasteCodeInEditor: async (code: string) => {
|
||||
@ -532,18 +563,74 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
})
|
||||
},
|
||||
|
||||
createNewFileAndSelect: async (name: string) => {
|
||||
return test?.step(`Create a file named ${name}, select it`, async () => {
|
||||
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
|
||||
.getByTestId('file-pane-scroll-container')
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: name })
|
||||
.click()
|
||||
})
|
||||
},
|
||||
|
||||
createNewFileAndSelect: async (name: string) => {
|
||||
return test?.step(`Create a file named ${name}, select it`, async () => {
|
||||
await openFilePanel(page)
|
||||
await page.getByTestId('create-file-button').click()
|
||||
await page.getByTestId('file-rename-field').fill(name)
|
||||
await page.keyboard.press('Enter')
|
||||
const newFile = page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: name })
|
||||
|
||||
await expect(newFile).toBeVisible()
|
||||
await newFile.click()
|
||||
})
|
||||
},
|
||||
|
||||
renameFile: async (fromName: string, toName: string) => {
|
||||
return test?.step(`Rename ${fromName} to ${toName}`, async () => {
|
||||
await page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: fromName })
|
||||
.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-rename').click()
|
||||
await page.getByTestId('file-rename-field').fill(toName)
|
||||
await page.keyboard.press('Enter')
|
||||
await page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: toName })
|
||||
.click()
|
||||
})
|
||||
},
|
||||
|
||||
deleteFile: async (name: string) => {
|
||||
return test?.step(`Delete ${name}`, async () => {
|
||||
await page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: name })
|
||||
.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-delete').click()
|
||||
await page.getByTestId('delete-confirmation').click()
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated Sorry I don't have time to fix this right now, but runs like
|
||||
* the one linked below show me that setting the open panes in this manner is not reliable.
|
||||
* You can either set `openPanes` as a part of the same initScript we run in setupElectron/setup,
|
||||
* or you can imperatively open the panes with functions like {openKclCodePanel}
|
||||
* (or we can make a general openPane function that takes a paneId).,
|
||||
* but having a separate initScript does not seem to work reliably.
|
||||
* @link https://github.com/KittyCAD/modeling-app/actions/runs/10731890169/job/29762700806?pr=3807#step:20:19553
|
||||
*/
|
||||
panesOpen: async (paneIds: PaneId[]) => {
|
||||
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
|
||||
await page.addInitScript(
|
||||
@ -764,7 +851,11 @@ export async function tearDown(page: Page, testInfo: TestInfo) {
|
||||
|
||||
// settingsOverrides may need to be augmented to take more generic items,
|
||||
// but we'll be strict for now
|
||||
export async function setup(context: BrowserContext, page: Page) {
|
||||
export async function setup(
|
||||
context: BrowserContext,
|
||||
page: Page,
|
||||
testInfo?: TestInfo
|
||||
) {
|
||||
await context.addInitScript(
|
||||
async ({ token, settingsKey, settings, IS_PLAYWRIGHT_KEY }) => {
|
||||
localStorage.clear()
|
||||
@ -772,7 +863,6 @@ export async function setup(context: BrowserContext, page: Page) {
|
||||
localStorage.setItem('persistCode', ``)
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
localStorage.setItem(IS_PLAYWRIGHT_KEY, 'true')
|
||||
console.log('TEST_SETTINGS.projects', settings)
|
||||
},
|
||||
{
|
||||
token: secrets.token,
|
||||
@ -801,6 +891,8 @@ export async function setup(context: BrowserContext, page: Page) {
|
||||
secure: true,
|
||||
},
|
||||
])
|
||||
|
||||
failOnConsoleErrors(page, testInfo)
|
||||
// kill animations, speeds up tests and reduced flakiness
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||
|
||||
@ -812,10 +904,12 @@ export async function setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn,
|
||||
cleanProjectDir = true,
|
||||
appSettings,
|
||||
}: {
|
||||
testInfo: TestInfo
|
||||
folderSetupFn?: (projectDirName: string) => Promise<void>
|
||||
cleanProjectDir?: boolean
|
||||
appSettings?: Partial<SaveSettingsPayload>
|
||||
}) {
|
||||
// create or otherwise clear the folder
|
||||
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
|
||||
@ -849,15 +943,19 @@ export async function setupElectron({
|
||||
|
||||
if (cleanProjectDir) {
|
||||
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
|
||||
const settingsOverrides = TOML.stringify({
|
||||
...TEST_SETTINGS,
|
||||
settings: {
|
||||
app: {
|
||||
...TEST_SETTINGS.app,
|
||||
projectDirectory: projectDirName,
|
||||
},
|
||||
},
|
||||
})
|
||||
const settingsOverrides = TOML.stringify(
|
||||
appSettings
|
||||
? { settings: appSettings }
|
||||
: {
|
||||
...TEST_SETTINGS,
|
||||
settings: {
|
||||
app: {
|
||||
...TEST_SETTINGS.app,
|
||||
projectDirectory: projectDirName,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
|
||||
}
|
||||
|
||||
@ -868,6 +966,48 @@ export async function setupElectron({
|
||||
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(
|
||||
element: Locator,
|
||||
container: Locator
|
||||
|
@ -3,8 +3,8 @@ import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { getUtils, setup, tearDown } from './test-utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
@ -12,8 +12,8 @@ test.afterEach(async ({ page }, testInfo) => {
|
||||
})
|
||||
|
||||
test.describe('Testing Camera Movement', () => {
|
||||
test('Can moving camera', async ({ page, context }) => {
|
||||
test.skip(process.platform === 'darwin', 'Can moving camera')
|
||||
test('Can move camera reliably', async ({ page, context }) => {
|
||||
test.skip(process.platform === 'darwin', 'Can move camera reliably')
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
@ -102,6 +102,13 @@ test.describe('Testing Camera Movement', () => {
|
||||
await bakeInRetries(async () => {
|
||||
await page.mouse.move(700, 200)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
const appLogoBBox = await page.getByTestId('app-logo').boundingBox()
|
||||
expect(appLogoBBox).not.toBeNull()
|
||||
if (!appLogoBBox) throw new Error('app logo not found')
|
||||
await page.mouse.move(
|
||||
appLogoBBox.x + appLogoBBox.width / 2,
|
||||
appLogoBBox.y + appLogoBBox.height / 2
|
||||
)
|
||||
await page.mouse.move(600, 303)
|
||||
await page.mouse.up({ button: 'right' })
|
||||
}, [4, -10.5, -120])
|
||||
@ -295,11 +302,11 @@ test.describe('Testing Camera Movement', () => {
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeVisible()
|
||||
await hoverOverNothing()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
|
||||
await page.waitForTimeout(400)
|
||||
|
||||
await hoverOverNothing()
|
||||
x = 975
|
||||
y = 468
|
||||
|
||||
|
@ -3,8 +3,8 @@ import { test, expect } from '@playwright/test'
|
||||
import { getUtils, setup, tearDown, TEST_COLORS } from './test-utils'
|
||||
import { XOR } from 'lib/utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
|
@ -4,8 +4,8 @@ import { getUtils, setup, tearDown } from './test-utils'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { TEST_CODE_GIZMO } from './storageStates'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
|
@ -4,8 +4,8 @@ import { deg, getUtils, setup, tearDown, wiggleMove } from './test-utils'
|
||||
import { LineInputsType } from 'lang/std/sketchcombos'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
|
@ -5,8 +5,8 @@ import { Coords2d } from 'lang/std/sketch'
|
||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
@ -31,12 +31,28 @@ test.describe('Testing selections', () => {
|
||||
|
||||
const xAxisClick = () =>
|
||||
page.mouse.click(700, 253).then(() => page.waitForTimeout(100))
|
||||
const xAxisClickAfterExitingSketch = () =>
|
||||
page.mouse.click(639, 278).then(() => page.waitForTimeout(100))
|
||||
const emptySpaceHover = () =>
|
||||
test.step('Hover over empty space', async () => {
|
||||
await page.mouse.move(700, 143, { steps: 5 })
|
||||
await expect(page.locator('.hover-highlight')).not.toBeVisible()
|
||||
})
|
||||
const emptySpaceClick = () =>
|
||||
page.mouse.click(700, 343).then(() => page.waitForTimeout(100))
|
||||
test.step(`Click in empty space`, async () => {
|
||||
await page.mouse.click(700, 143)
|
||||
await expect(page.locator('.cm-line').last()).toHaveClass(
|
||||
/cm-activeLine/
|
||||
)
|
||||
})
|
||||
const topHorzSegmentClick = () =>
|
||||
page.mouse.click(709, 290).then(() => page.waitForTimeout(100))
|
||||
page.mouse
|
||||
.click(startXPx, 500 - PUR * 20)
|
||||
.then(() => page.waitForTimeout(100))
|
||||
const bottomHorzSegmentClick = () =>
|
||||
page.mouse.click(767, 396).then(() => page.waitForTimeout(100))
|
||||
page.mouse
|
||||
.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
.then(() => page.waitForTimeout(100))
|
||||
|
||||
await u.clearCommandLogs()
|
||||
await expect(
|
||||
@ -171,7 +187,9 @@ test.describe('Testing selections', () => {
|
||||
await emptySpaceClick()
|
||||
}
|
||||
|
||||
await selectionSequence()
|
||||
await test.step(`Test hovering and selecting on fresh sketch`, async () => {
|
||||
await selectionSequence()
|
||||
})
|
||||
|
||||
// hovering in fresh sketch worked, lets try exiting and re-entering
|
||||
await u.openAndClearDebugPanel()
|
||||
@ -184,16 +202,17 @@ test.describe('Testing selections', () => {
|
||||
|
||||
// select a line, this verifies that sketches in the scene can be selected outside of sketch mode
|
||||
await topHorzSegmentClick()
|
||||
await xAxisClickAfterExitingSketch()
|
||||
await page.waitForTimeout(100)
|
||||
await emptySpaceHover()
|
||||
|
||||
// enter sketch again
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
|
||||
'default_camera_get_settings'
|
||||
)
|
||||
await page.waitForTimeout(150)
|
||||
|
||||
await page.waitForTimeout(300) // wait for animation
|
||||
await page.waitForTimeout(450) // wait for animation
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.sendCustomCmd({
|
||||
@ -220,8 +239,9 @@ test.describe('Testing selections', () => {
|
||||
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// hover again and check it works
|
||||
await selectionSequence()
|
||||
await test.step(`Test hovering and selecting on edited sketch`, async () => {
|
||||
await selectionSequence()
|
||||
})
|
||||
})
|
||||
|
||||
test('Solids should be select and deletable', async ({ page }) => {
|
||||
@ -413,7 +433,7 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
|
||||
|> line([0, 20.03], %)
|
||||
|> line([62.61, 0], %, $seg03)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
|> close(%)
|
||||
`
|
||||
)
|
||||
}, KCL_DEFAULT_LENGTH)
|
||||
@ -516,11 +536,22 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
|
||||
await page.waitForTimeout(100)
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const extrusionTop: Coords2d = [800, 240]
|
||||
const extrusionTopCap: Coords2d = [800, 240]
|
||||
const flatExtrusionFace: Coords2d = [960, 160]
|
||||
const arc: Coords2d = [840, 160]
|
||||
const tangentialArcTo: Coords2d = [840, 160]
|
||||
const close: Coords2d = [720, 200]
|
||||
const nothing: Coords2d = [600, 200]
|
||||
const closeEdge: Coords2d = [744, 233]
|
||||
const closeAdjacentEdge: Coords2d = [688, 123]
|
||||
const closeOppositeEdge: Coords2d = [687, 169]
|
||||
|
||||
const tangentialArcEdge: Coords2d = [811, 142]
|
||||
const tangentialArcOppositeEdge: Coords2d = [820, 180]
|
||||
const tangentialArcAdjacentEdge: Coords2d = [893, 165]
|
||||
|
||||
const straightSegmentEdge: Coords2d = [819, 369]
|
||||
const straightSegmentOppositeEdge: Coords2d = [635, 394]
|
||||
const straightSegmentAdjacentEdge: Coords2d = [679, 329]
|
||||
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await page.mouse.click(nothing[0], nothing[1])
|
||||
@ -528,26 +559,141 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await page.mouse.move(extrusionTop[0], extrusionTop[1])
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
|
||||
const checkCodeAtHoverPosition = async (
|
||||
name = '',
|
||||
coord: Coords2d,
|
||||
highlightCode: string,
|
||||
activeLine = highlightCode
|
||||
) => {
|
||||
await test.step(`test selection for: ${name}`, async () => {
|
||||
const highlightedLocator = page.getByTestId('hover-highlight')
|
||||
const activeLineLocator = page.locator('.cm-activeLine')
|
||||
|
||||
await page.mouse.move(arc[0], arc[1])
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
|
||||
await test.step(`hover should highlight correct code`, async () => {
|
||||
await page.mouse.move(coord[0], coord[1])
|
||||
await expect(highlightedLocator.first()).toBeVisible()
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const textContents = await highlightedLocator.allTextContents()
|
||||
return textContents.join('').replace(/\s+/g, '')
|
||||
})
|
||||
.toBe(highlightCode)
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
})
|
||||
await test.step(`click should put the cursor in the right place`, async () => {
|
||||
await expect(highlightedLocator.first()).not.toBeVisible()
|
||||
await page.mouse.click(coord[0], coord[1])
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const activeLines = await activeLineLocator.allInnerTexts()
|
||||
return activeLines.join('')
|
||||
})
|
||||
.toContain(activeLine)
|
||||
// check pixels near the click location are yellow
|
||||
})
|
||||
await test.step(`check the engine agrees with selections`, async () => {
|
||||
// ultimately the only way we know if the engine agrees with the selection from the FE
|
||||
// perspective is if it highlights the pixels near where we clicked yellow.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const RGBs = await u.getPixelRGBs({ x: coord[0], y: coord[1] }, 3)
|
||||
for (const rgb of RGBs) {
|
||||
const [r, g, b] = rgb
|
||||
const RGAverage = (r + g) / 2
|
||||
const isRedGreenSameIsh = Math.abs(r - g) < 3
|
||||
const isBlueLessThanRG = RGAverage - b > 45
|
||||
const isYellowy = isRedGreenSameIsh && isBlueLessThanRG
|
||||
if (isYellowy) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
.toBeTruthy()
|
||||
await page.mouse.click(nothing[0], nothing[1])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
await page.mouse.move(close[0], close[1])
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
|
||||
await checkCodeAtHoverPosition(
|
||||
'extrusionTopCap',
|
||||
extrusionTopCap,
|
||||
'startProfileAt([20,0],%)',
|
||||
'startProfileAt([20, 0], %)'
|
||||
)
|
||||
await checkCodeAtHoverPosition(
|
||||
'flatExtrusionFace',
|
||||
flatExtrusionFace,
|
||||
`angledLineThatIntersects({angle:3.14,intersectTag:a,offset:0},%)extrude(5+7,%)`,
|
||||
'}, %)'
|
||||
)
|
||||
|
||||
await page.mouse.move(flatExtrusionFace[0], flatExtrusionFace[1])
|
||||
await expect(page.getByTestId('hover-highlight')).toHaveCount(6) // multiple lines
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
|
||||
await checkCodeAtHoverPosition(
|
||||
'tangentialArcTo',
|
||||
tangentialArcTo,
|
||||
'tangentialArcTo([13.14+0,13.14],%)extrude(5+7,%)',
|
||||
'tangentialArcTo([13.14 + 0, 13.14], %)'
|
||||
)
|
||||
await checkCodeAtHoverPosition(
|
||||
'tangentialArcEdge',
|
||||
tangentialArcEdge,
|
||||
`tangentialArcTo([13.14+0,13.14],%)`,
|
||||
'tangentialArcTo([13.14 + 0, 13.14], %)'
|
||||
)
|
||||
await checkCodeAtHoverPosition(
|
||||
'tangentialArcOppositeEdge',
|
||||
tangentialArcOppositeEdge,
|
||||
`tangentialArcTo([13.14+0,13.14],%)`,
|
||||
'tangentialArcTo([13.14 + 0, 13.14], %)'
|
||||
)
|
||||
await checkCodeAtHoverPosition(
|
||||
'tangentialArcAdjacentEdge',
|
||||
tangentialArcAdjacentEdge,
|
||||
`tangentialArcTo([13.14+0,13.14],%)`,
|
||||
'tangentialArcTo([13.14 + 0, 13.14], %)'
|
||||
)
|
||||
|
||||
await checkCodeAtHoverPosition(
|
||||
'close',
|
||||
close,
|
||||
'close(%)extrude(5+7,%)',
|
||||
'close(%)'
|
||||
)
|
||||
await checkCodeAtHoverPosition(
|
||||
'closeEdge',
|
||||
closeEdge,
|
||||
`close(%)`,
|
||||
'close(%)'
|
||||
)
|
||||
await checkCodeAtHoverPosition(
|
||||
'closeAdjacentEdge',
|
||||
closeAdjacentEdge,
|
||||
`close(%)`,
|
||||
'close(%)'
|
||||
)
|
||||
await checkCodeAtHoverPosition(
|
||||
'closeOppositeEdge',
|
||||
closeOppositeEdge,
|
||||
`close(%)`,
|
||||
'close(%)'
|
||||
)
|
||||
|
||||
await checkCodeAtHoverPosition(
|
||||
'straightSegmentEdge',
|
||||
straightSegmentEdge,
|
||||
`angledLineToY({angle:30,to:11.14},%)`,
|
||||
'angledLineToY({ angle: 30, to: 11.14 }, %)'
|
||||
)
|
||||
await checkCodeAtHoverPosition(
|
||||
'straightSegmentOppositeEdge',
|
||||
straightSegmentOppositeEdge,
|
||||
`angledLineToY({angle:30,to:11.14},%)`,
|
||||
'angledLineToY({ angle: 30, to: 11.14 }, %)'
|
||||
)
|
||||
await checkCodeAtHoverPosition(
|
||||
'straightSegmentAdjancentEdge',
|
||||
straightSegmentAdjacentEdge,
|
||||
`angledLineToY({angle:30,to:11.14},%)`,
|
||||
'angledLineToY({ angle: 30, to: 11.14 }, %)'
|
||||
)
|
||||
})
|
||||
test("Extrude button should be disabled if there's no extrudable geometry when nothing is selected", async ({
|
||||
page,
|
||||
@ -773,9 +919,9 @@ const extrude001 = extrude(50, sketch001)
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
let noHoverColor: [number, number, number] = [82, 82, 82]
|
||||
let hoverColor: [number, number, number] = [116, 116, 116]
|
||||
let selectColor: [number, number, number] = [144, 148, 97]
|
||||
let noHoverColor: [number, number, number] = [92, 92, 92]
|
||||
let hoverColor: [number, number, number] = [127, 127, 127]
|
||||
let selectColor: [number, number, number] = [155, 155, 105]
|
||||
|
||||
const extrudeWall = { x: 670, y: 275 }
|
||||
const extrudeText = `line([170.36, -121.61], %, $seg01)`
|
||||
@ -787,7 +933,7 @@ const extrude001 = extrude(50, sketch001)
|
||||
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
|
||||
.toBeLessThan(5)
|
||||
.toBeLessThan(15)
|
||||
await page.mouse.move(nothing.x, nothing.y)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(extrudeWall.x, extrudeWall.y)
|
||||
@ -798,43 +944,43 @@ const extrude001 = extrude(50, sketch001)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(extrudeWall, hoverColor)
|
||||
).toBeLessThan(6)
|
||||
).toBeLessThan(15)
|
||||
await page.mouse.click(extrudeWall.x, extrudeWall.y)
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(extrudeWall, selectColor)
|
||||
).toBeLessThan(6)
|
||||
).toBeLessThan(15)
|
||||
await page.waitForTimeout(1000)
|
||||
// check color stays there, i.e. not overridden (this was a bug previously)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(extrudeWall, selectColor)
|
||||
).toBeLessThan(6)
|
||||
).toBeLessThan(15)
|
||||
|
||||
await page.mouse.move(nothing.x, nothing.y)
|
||||
await page.waitForTimeout(300)
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
// because of shading, color is not exact everywhere on the face
|
||||
noHoverColor = [104, 104, 104]
|
||||
hoverColor = [134, 134, 134]
|
||||
selectColor = [158, 162, 110]
|
||||
noHoverColor = [115, 115, 115]
|
||||
hoverColor = [145, 145, 145]
|
||||
selectColor = [168, 168, 120]
|
||||
|
||||
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(6)
|
||||
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(15)
|
||||
await page.mouse.move(cap.x, cap.y)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toContainText(
|
||||
removeAfterFirstParenthesis(capText)
|
||||
)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(6)
|
||||
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15)
|
||||
await page.mouse.click(cap.x, cap.y)
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
|
||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
|
||||
await page.waitForTimeout(1000)
|
||||
// check color stays there, i.e. not overridden (this was a bug previously)
|
||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(6)
|
||||
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
|
||||
})
|
||||
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
|
||||
page,
|
||||
@ -876,7 +1022,7 @@ const extrude001 = extrude(50, sketch001)
|
||||
|> line([4.95, -8], %)
|
||||
|> line([-20.38, -10.12], %)
|
||||
|> line([-15.79, 17.08], %)
|
||||
|
||||
|
||||
fn yohey = (pos) => {
|
||||
const sketch004 = startSketchOn('XZ')
|
||||
${extrudeAndEditBlockedInFunction}
|
||||
@ -886,7 +1032,7 @@ const extrude001 = extrude(50, sketch001)
|
||||
|> line([-15.79, 17.08], %)
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
yohey([15.79, -34.6])
|
||||
`
|
||||
)
|
||||
|
@ -8,12 +8,16 @@ import {
|
||||
tearDown,
|
||||
executorInputPath,
|
||||
} from './test-utils'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates'
|
||||
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
|
||||
import {
|
||||
TEST_SETTINGS_KEY,
|
||||
TEST_SETTINGS_CORRUPTED,
|
||||
TEST_SETTINGS,
|
||||
} from './storageStates'
|
||||
import * as TOML from '@iarna/toml'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
@ -65,12 +69,15 @@ test.describe('Testing settings', () => {
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await page
|
||||
.getByRole('button', { name: 'Start Sketch' })
|
||||
.waitFor({ state: 'visible' })
|
||||
await test.step(`Setup`, async () => {
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await page
|
||||
.getByRole('button', { name: 'Start Sketch' })
|
||||
.waitFor({ state: 'visible' })
|
||||
})
|
||||
|
||||
// Selectors and constants
|
||||
const paneButtonLocator = page.getByTestId('debug-pane-button')
|
||||
const headingLocator = page.getByRole('heading', {
|
||||
name: 'Settings',
|
||||
@ -78,11 +85,23 @@ test.describe('Testing settings', () => {
|
||||
})
|
||||
const inputLocator = page.locator('input[name="modeling-showDebugPanel"]')
|
||||
|
||||
// Open the settings modal with the browser keyboard shortcut
|
||||
await page.keyboard.press('ControlOrMeta+Shift+,')
|
||||
await test.step('Open settings dialog and set "Show debug panel" to on', async () => {
|
||||
await page.keyboard.press('ControlOrMeta+Shift+,')
|
||||
await expect(headingLocator).toBeVisible()
|
||||
|
||||
await expect(headingLocator).toBeVisible()
|
||||
await page.locator('#showDebugPanel').getByText('OffOn').click()
|
||||
/** Test to close https://github.com/KittyCAD/modeling-app/issues/2713 */
|
||||
await test.step(`Confirm that this dialog has a solid background`, async () => {
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff({ x: 600, y: 250 }, [28, 28, 28]), {
|
||||
timeout: 1000,
|
||||
message:
|
||||
'Checking for solid background, should not see default plane colors',
|
||||
})
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await page.locator('#showDebugPanel').getByText('OffOn').click()
|
||||
})
|
||||
|
||||
// Close it and open again with keyboard shortcut, while KCL editor is focused
|
||||
// Put the cursor in the editor
|
||||
@ -154,29 +173,33 @@ test.describe('Testing settings', () => {
|
||||
|
||||
test('Project and user settings can be reset', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await page
|
||||
.getByRole('button', { name: 'Start Sketch' })
|
||||
.waitFor({ state: 'visible' })
|
||||
await test.step(`Setup`, async () => {
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
})
|
||||
|
||||
// Selectors and constants
|
||||
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
|
||||
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
||||
const resetButton = page.getByRole('button', {
|
||||
name: 'Restore default settings',
|
||||
})
|
||||
const resetButton = (level: SettingsLevel) =>
|
||||
page.getByRole('button', {
|
||||
name: `Reset ${level}-level settings`,
|
||||
})
|
||||
const themeColorSetting = page.locator('#themeColor').getByRole('slider')
|
||||
const settingValues = {
|
||||
default: '259',
|
||||
user: '120',
|
||||
project: '50',
|
||||
}
|
||||
const resetToast = (level: SettingsLevel) =>
|
||||
page.getByText(`${level}-level settings were reset`)
|
||||
|
||||
// Open the settings modal with lower-right button
|
||||
await page.getByRole('link', { name: 'Settings' }).last().click()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||
).toBeVisible()
|
||||
await test.step(`Open the settings modal`, async () => {
|
||||
await page.getByRole('link', { name: 'Settings' }).last().click()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Set up theme color', async () => {
|
||||
// Verify we're looking at the project-level settings,
|
||||
@ -195,37 +218,40 @@ test.describe('Testing settings', () => {
|
||||
|
||||
await test.step('Reset project settings', async () => {
|
||||
// Click the reset settings button.
|
||||
await resetButton.click()
|
||||
await resetButton('project').click()
|
||||
|
||||
await expect(page.getByText('Settings restored to default')).toBeVisible()
|
||||
await expect(
|
||||
page.getByText('Settings restored to default')
|
||||
).not.toBeVisible()
|
||||
await expect(resetToast('project')).toBeVisible()
|
||||
await expect(resetToast('project')).not.toBeVisible()
|
||||
|
||||
// Verify it is now set to the inherited user value
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.user)
|
||||
|
||||
// Check that the user setting also rolled back
|
||||
await userSettingsTab.click()
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
||||
await projectSettingsTab.click()
|
||||
await test.step(`Check that the user settings did not change`, async () => {
|
||||
await userSettingsTab.click()
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.user)
|
||||
})
|
||||
|
||||
// Set project-level value to 50 again to test the user-level reset
|
||||
await themeColorSetting.fill(settingValues.project)
|
||||
await userSettingsTab.click()
|
||||
await test.step(`Set project-level again to test the user-level reset`, async () => {
|
||||
await projectSettingsTab.click()
|
||||
await themeColorSetting.fill(settingValues.project)
|
||||
await userSettingsTab.click()
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Reset user settings', async () => {
|
||||
// Change the setting and click the reset settings button.
|
||||
await themeColorSetting.fill(settingValues.user)
|
||||
await resetButton.click()
|
||||
// Click the reset settings button.
|
||||
await resetButton('user').click()
|
||||
|
||||
await expect(resetToast('user')).toBeVisible()
|
||||
await expect(resetToast('user')).not.toBeVisible()
|
||||
|
||||
// Verify it is now set to the default value
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
||||
|
||||
// Check that the project setting also changed
|
||||
await projectSettingsTab.click()
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
||||
await test.step(`Check that the project settings did not change`, async () => {
|
||||
await projectSettingsTab.click()
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.project)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -288,7 +314,7 @@ test.describe('Testing settings', () => {
|
||||
})
|
||||
|
||||
await test.step('Refresh the application and see project setting applied', async () => {
|
||||
await page.reload()
|
||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||
|
||||
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
|
||||
await settingsCloseButton.click()
|
||||
@ -303,53 +329,109 @@ 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(
|
||||
`Closing settings modal should go back to the original file being viewed`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
folderSetupFn: async (dir) => {
|
||||
const bracketDir = join(dir, 'project-000')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cube.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
join(bracketDir, '2.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const kclCube = await fsp.readFile(executorInputPath('cube.kcl'), 'utf-8')
|
||||
const kclCylinder = await fsp.readFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
const {
|
||||
panesOpen,
|
||||
createAndSelectProject,
|
||||
pasteCodeInEditor,
|
||||
clickPane,
|
||||
createNewFileAndSelect,
|
||||
openKclCodePanel,
|
||||
openFilePanel,
|
||||
waitForPageLoad,
|
||||
selectFile,
|
||||
editorTextMatches,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await panesOpen([])
|
||||
|
||||
await test.step('Precondition: No projects exist', async () => {
|
||||
await test.step('Precondition: Open to second project file', async () => {
|
||||
await expect(page.getByTestId('home-section')).toBeVisible()
|
||||
const projectLinksPre = page.getByTestId('project-link')
|
||||
await expect(projectLinksPre).toHaveCount(0)
|
||||
await page.getByText('project-000').click()
|
||||
await waitForPageLoad()
|
||||
await openKclCodePanel()
|
||||
await openFilePanel()
|
||||
await editorTextMatches(kclCube)
|
||||
|
||||
await selectFile('2.kcl')
|
||||
await editorTextMatches(kclCylinder)
|
||||
})
|
||||
|
||||
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', {
|
||||
name: 'settings Settings',
|
||||
})
|
||||
@ -357,6 +439,9 @@ test.describe('Testing settings', () => {
|
||||
|
||||
await test.step('Open and close settings', async () => {
|
||||
await settingsOpenButton.click()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||
).toBeVisible()
|
||||
await settingsCloseButton.click()
|
||||
})
|
||||
|
||||
@ -370,25 +455,37 @@ test.describe('Testing settings', () => {
|
||||
|
||||
test('Changing modeling default unit', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await page
|
||||
.getByRole('button', { name: 'Start Sketch' })
|
||||
.waitFor({ state: 'visible' })
|
||||
|
||||
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
||||
|
||||
// Open the settings modal with lower-right button
|
||||
await page.getByRole('link', { name: 'Settings' }).last().click()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||
).toBeVisible()
|
||||
|
||||
const resetButton = page.getByRole('button', {
|
||||
name: 'Restore default settings',
|
||||
await test.step(`Test setup`, async () => {
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await page
|
||||
.getByRole('button', { name: 'Start Sketch' })
|
||||
.waitFor({ state: 'visible' })
|
||||
})
|
||||
|
||||
// Selectors and constants
|
||||
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
||||
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
|
||||
const defaultUnitSection = page.getByText(
|
||||
'default unitRoll back default unitRoll back to match'
|
||||
)
|
||||
const defaultUnitRollbackButton = page.getByRole('button', {
|
||||
name: 'Roll back default unit',
|
||||
})
|
||||
|
||||
await test.step(`Open the settings modal`, async () => {
|
||||
await page.getByRole('link', { name: 'Settings' }).last().click()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Settings', exact: true })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step(`Reset unit setting`, async () => {
|
||||
await userSettingsTab.click()
|
||||
await defaultUnitSection.hover()
|
||||
await defaultUnitRollbackButton.click()
|
||||
await projectSettingsTab.click()
|
||||
})
|
||||
// Default unit should be mm
|
||||
await resetButton.click()
|
||||
|
||||
await test.step('Change modeling default unit within project tab', async () => {
|
||||
const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => {
|
||||
@ -493,4 +590,148 @@ test.describe('Testing settings', () => {
|
||||
await changeUnitOfMeasureInGizmo('m', 'Meters')
|
||||
})
|
||||
})
|
||||
|
||||
test('Changing theme in sketch mode', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([5, 0], %)
|
||||
|> line([0, 5], %)
|
||||
|> line([-5, 0], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(5, sketch001)
|
||||
`
|
||||
)
|
||||
})
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
// Selectors and constants
|
||||
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
|
||||
const lineToolButton = page.getByTestId('line')
|
||||
const segmentOverlays = page.getByTestId('segment-overlay')
|
||||
const sketchOriginLocation = { x: 600, y: 250 }
|
||||
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
|
||||
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
|
||||
|
||||
await test.step(`Get into sketch mode`, async () => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await page.mouse.click(700, 200)
|
||||
await expect(editSketchButton).toBeVisible()
|
||||
await editSketchButton.click()
|
||||
|
||||
// We use the line tool as a proxy for sketch mode
|
||||
await expect(lineToolButton).toBeVisible()
|
||||
await expect(segmentOverlays).toHaveCount(4)
|
||||
// but we allow more time to pass for animating to the sketch
|
||||
await page.waitForTimeout(1000)
|
||||
})
|
||||
|
||||
await test.step(`Check the sketch line color before`, async () => {
|
||||
await expect
|
||||
.poll(() =>
|
||||
u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor)
|
||||
)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await test.step(`Change theme to light using command palette`, async () => {
|
||||
await page.keyboard.press('ControlOrMeta+K')
|
||||
await page.getByRole('option', { name: 'theme' }).click()
|
||||
await page.getByRole('option', { name: 'light' }).click()
|
||||
await expect(page.getByText('theme to "light"')).toBeVisible()
|
||||
|
||||
// Make sure we haven't left sketch mode
|
||||
await expect(lineToolButton).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step(`Check the sketch line color after`, async () => {
|
||||
await expect
|
||||
.poll(() =>
|
||||
u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor)
|
||||
)
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Turning off "Show debug panel" with debug panel open leaves no phantom panel`, async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Override beforeEach test setup
|
||||
// with debug panel open
|
||||
// but "show debug panel" set to false
|
||||
await page.addInitScript(
|
||||
async ({ settingsKey, settings }) => {
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
localStorage.setItem(
|
||||
'persistModelingContext',
|
||||
'{"openPanes":["debug"]}'
|
||||
)
|
||||
},
|
||||
{
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({
|
||||
settings: {
|
||||
...TEST_SETTINGS,
|
||||
modeling: { ...TEST_SETTINGS.modeling, showDebugPanel: false },
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
// Constants and locators
|
||||
const resizeHandle = page.locator('.sidebar-resize-handles > div.block')
|
||||
const debugPaneButton = page.getByTestId('debug-pane-button')
|
||||
const commandsButton = page.getByRole('button', { name: 'Commands' })
|
||||
const debugPaneOption = page.getByRole('option', {
|
||||
name: 'Settings · modeling · show debug panel',
|
||||
})
|
||||
|
||||
async function setShowDebugPanelTo(value: 'On' | 'Off') {
|
||||
await commandsButton.click()
|
||||
await debugPaneOption.click()
|
||||
await page.getByRole('option', { name: value }).click()
|
||||
await expect(
|
||||
page.getByText(
|
||||
`Set show debug panel to "${value === 'On'}" for this project`
|
||||
)
|
||||
).toBeVisible()
|
||||
}
|
||||
|
||||
await test.step(`Initial load with corrupted settings`, async () => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
// Check that the debug panel is not visible
|
||||
await expect(debugPaneButton).not.toBeVisible()
|
||||
// Check the pane resize handle wrapper is not visible
|
||||
await expect(resizeHandle).not.toBeVisible()
|
||||
})
|
||||
|
||||
await test.step(`Open code pane to verify we see the resize handles`, async () => {
|
||||
await u.openKclCodePanel()
|
||||
await expect(resizeHandle).toBeVisible()
|
||||
await u.closeKclCodePanel()
|
||||
})
|
||||
|
||||
await test.step(`Turn on debug panel, open it`, async () => {
|
||||
await setShowDebugPanelTo('On')
|
||||
await expect(debugPaneButton).toBeVisible()
|
||||
// We want the logic to clear the phantom panel, so we shouldn't see
|
||||
// the real panel (and therefore the resize handle) yet
|
||||
await expect(resizeHandle).not.toBeVisible()
|
||||
await u.openDebugPanel()
|
||||
await expect(resizeHandle).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step(`Turn off debug panel setting with it open`, async () => {
|
||||
await setShowDebugPanelTo('Off')
|
||||
await expect(debugPaneButton).not.toBeVisible()
|
||||
await expect(resizeHandle).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,11 +1,5 @@
|
||||
import { test, expect, Page } from '@playwright/test'
|
||||
import {
|
||||
getUtils,
|
||||
setup,
|
||||
tearDown,
|
||||
setupElectron,
|
||||
createProjectAndRenameIt,
|
||||
} from './test-utils'
|
||||
import { getUtils, setup, tearDown, setupElectron } from './test-utils'
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
@ -540,7 +534,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
|
||||
// Ensure the final toast remains.
|
||||
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
|
||||
await expect(page.getByText(`a 2x8 lego`)).not.toBeVisible()
|
||||
await expect(page.getByText(`Prompt: "a 2x8 lego`)).not.toBeVisible()
|
||||
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
|
||||
|
||||
// Ensure you can copy the code for the final model.
|
||||
@ -696,41 +690,53 @@ test(
|
||||
'Text-to-CAD functionality',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const projectName = 'project-000'
|
||||
const prompt = 'lego 2x4'
|
||||
const textToCadFileName = 'lego-2x4.kcl'
|
||||
|
||||
const { electronApp, page, dir } = await setupElectron({ testInfo })
|
||||
const fileExists = () =>
|
||||
fs.existsSync(join(dir, 'test-000', 'lego-2x4.kcl'))
|
||||
fs.existsSync(join(dir, projectName, textToCadFileName))
|
||||
|
||||
const {
|
||||
createAndSelectProject,
|
||||
openFilePanel,
|
||||
openKclCodePanel,
|
||||
waitForPageLoad,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
// Locators
|
||||
const projectMenuButton = page.getByRole('button', { name: projectName })
|
||||
const textToCadFileButton = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: textToCadFileName }),
|
||||
})
|
||||
const textToCadComment = page.getByText(
|
||||
`// Generated by Text-to-CAD: ${prompt}`
|
||||
)
|
||||
|
||||
// Create and navigate to the project
|
||||
await createProjectAndRenameIt({ name: 'test-000', page })
|
||||
await page.getByTestId('project-link').click()
|
||||
await createAndSelectProject('project-000')
|
||||
|
||||
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// Open the files pane
|
||||
const filesPaneButton = page.getByTestId('files-pane-button')
|
||||
await filesPaneButton.click()
|
||||
await waitForPageLoad()
|
||||
await openFilePanel()
|
||||
await openKclCodePanel()
|
||||
|
||||
await test.step(`Test file creation`, async () => {
|
||||
await sendPromptFromCommandBar(page, 'lego 2x4')
|
||||
await sendPromptFromCommandBar(page, prompt)
|
||||
// File is considered created if it shows up in the Project Files pane
|
||||
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
|
||||
await expect(file).toBeVisible({ timeout: 20_000 })
|
||||
await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
|
||||
expect(fileExists()).toBeTruthy()
|
||||
})
|
||||
|
||||
await test.step(`Test file navigation`, async () => {
|
||||
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
|
||||
await file.click()
|
||||
const kclComment = page.getByText('Lego 2x4 Brick')
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await textToCadFileButton.click()
|
||||
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane
|
||||
await expect(kclComment).toBeVisible({ timeout: 20_000 })
|
||||
await expect(textToCadComment).toBeVisible({ timeout: 20_000 })
|
||||
await expect(projectMenuButton).toContainText(textToCadFileName)
|
||||
})
|
||||
|
||||
await test.step(`Test file deletion on rejection`, async () => {
|
||||
@ -744,6 +750,8 @@ test(
|
||||
)
|
||||
await expect(submittingToastMessage).toBeVisible()
|
||||
expect(fileExists()).toBeFalsy()
|
||||
// Confirm we've navigated back to the main.kcl file after deletion
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
|
@ -2,8 +2,8 @@ import { test, expect } from '@playwright/test'
|
||||
|
||||
import { doExport, getUtils, makeTemplate, setup, tearDown } from './test-utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
|
75
electron-builder.yml
Normal file
@ -0,0 +1,75 @@
|
||||
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
|
@ -15,6 +15,7 @@
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href="./inter/inter.css" />
|
||||
<link rel="stylesheet" href="https://use.typekit.net/zzv8rvm.css" />
|
||||
<script
|
||||
defer
|
||||
|
8
installer.nsh
Normal file
@ -0,0 +1,8 @@
|
||||
!macro preInit
|
||||
SetRegView 64
|
||||
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
|
||||
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
|
||||
SetRegView 32
|
||||
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
|
||||
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App"
|
||||
!macroend
|
2
interface.d.ts
vendored
@ -30,8 +30,6 @@ export interface IElectronAPI {
|
||||
join: typeof path.join
|
||||
sep: typeof path.sep
|
||||
rename: (prev: string, next: string) => typeof fs.rename
|
||||
setBaseUrl: (value: string) => void
|
||||
loadProjectAtStartup: () => Promise<ProjectState | null>
|
||||
packageJson: {
|
||||
name: string
|
||||
}
|
||||
|
25
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zoo-modeling-app",
|
||||
"version": "0.24.12",
|
||||
"version": "0.25.2",
|
||||
"private": true,
|
||||
"productName": "Zoo Modeling App",
|
||||
"author": {
|
||||
@ -34,11 +34,12 @@
|
||||
"@ts-stack/markdown": "^1.5.0",
|
||||
"@tweenjs/tween.js": "^23.1.1",
|
||||
"@xstate/inspect": "^0.8.0",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"@xstate/react": "^4.1.1",
|
||||
"bonjour-service": "^1.2.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-updater": "^6.3.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.5.8",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
@ -50,7 +51,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-hotkeys-hook": "^4.5.1",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-modal-promise": "^1.0.2",
|
||||
@ -63,7 +64,7 @@
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"vscode-uri": "^3.0.8",
|
||||
"web-vitals": "^3.5.2",
|
||||
"xstate": "^4.38.2"
|
||||
"xstate": "^5.17.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
@ -87,7 +88,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",
|
||||
"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",
|
||||
"lint": "eslint --fix src e2e",
|
||||
"lint": "eslint --fix src e2e packages/codemirror-lsp-client",
|
||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
|
||||
"postinstall": "yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
|
||||
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
|
||||
@ -97,7 +98,9 @@
|
||||
"tron:package": "electron-forge package",
|
||||
"tron:make": "electron-forge make",
|
||||
"tron:publish": "electron-forge publish",
|
||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron"
|
||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
|
||||
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
||||
"tronb:package": "electron-builder --config electron-builder.yml"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
@ -150,7 +153,6 @@
|
||||
"@types/three": "^0.163.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
@ -161,10 +163,12 @@
|
||||
"autoprefixer": "^10.4.19",
|
||||
"d3-force": "^3.0.0",
|
||||
"electron": "^32.0.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
"eslint-plugin-import": "^2.25.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
||||
"happy-dom": "^14.3.10",
|
||||
"http-server": "^14.1.1",
|
||||
@ -172,20 +176,19 @@
|
||||
"node-fetch": "^3.3.2",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss": "^8.4.43",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"setimmediate": "^1.0.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.4.2",
|
||||
"vite": "^5.4.3",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-webgl-canvas-mock": "^1.1.0",
|
||||
"wait-on": "^7.2.0",
|
||||
"wasm-pack": "^0.13.0",
|
||||
"ws": "^8.17.0",
|
||||
"yarn": "^1.22.22"
|
||||
|
@ -72,6 +72,7 @@ export class LanguageServerClient {
|
||||
async initialize() {
|
||||
// Start the client in the background.
|
||||
this.client.setNotifyFn(this.processNotifications.bind(this))
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.client.start()
|
||||
|
||||
this.ready = true
|
||||
@ -195,6 +196,9 @@ export class LanguageServerClient {
|
||||
}
|
||||
|
||||
private processNotifications(notification: LSP.NotificationMessage) {
|
||||
for (const plugin of this.plugins) plugin.processNotification(notification)
|
||||
for (const plugin of this.plugins) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
plugin.processNotification(notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ export default function lspFormatExt(
|
||||
run: (view: EditorView) => {
|
||||
let value = view.plugin(plugin)
|
||||
if (!value) return false
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
value.requestFormatting()
|
||||
return true
|
||||
},
|
||||
|
@ -117,6 +117,7 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
|
||||
this.processLspNotification = options.processLspNotification
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.initialize({
|
||||
documentText: this.getDocText(),
|
||||
})
|
||||
@ -149,6 +150,7 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
}
|
||||
|
||||
async initialize({ documentText }: { documentText: string }) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
if (this.client.initializePromise) {
|
||||
await this.client.initializePromise
|
||||
}
|
||||
@ -162,7 +164,9 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
},
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.requestSemanticTokens()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.updateFoldingRanges()
|
||||
}
|
||||
|
||||
@ -225,7 +229,9 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
contentChanges: [{ text: this.view.state.doc.toString() }],
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.requestSemanticTokens()
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.updateFoldingRanges()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@ -526,7 +532,9 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
processDiagnostics(params: PublishDiagnosticsParams) {
|
||||
if (params.uri !== this.getDocUri()) return
|
||||
|
||||
const diagnostics = params.diagnostics
|
||||
// Commented to avoid the lint. See TODO below.
|
||||
// const diagnostics =
|
||||
params.diagnostics
|
||||
.map(({ range, message, severity }) => ({
|
||||
from: posToOffset(this.view.state.doc, range.start)!,
|
||||
to: posToOffset(this.view.state.doc, range.end)!,
|
||||
|
@ -1,31 +0,0 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
timeout: 120_000, // override the default 30s timeout
|
||||
testDir: './e2e/playwright',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Do not retry */
|
||||
retries: process.env.CI ? 0 : 0,
|
||||
/* Different amount of parallelism on CI and local. */
|
||||
workers: process.env.CI ? 1 : 4,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [
|
||||
[process.env.CI ? 'dot' : 'list'],
|
||||
['json', { outputFile: './test-results/report.json' }],
|
||||
['html'],
|
||||
],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'retain-on-failure',
|
||||
actionTimeout: 15000,
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
})
|
@ -57,10 +57,10 @@ export default defineConfig({
|
||||
},
|
||||
}, // or 'chrome-beta'
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: { ...devices['Desktop Safari'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
|
BIN
public/inter/InterVariable-Italic.woff2
Normal file
BIN
public/inter/InterVariable.woff2
Normal file
14
public/inter/inter.css
Normal file
@ -0,0 +1,14 @@
|
||||
@font-face {
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url("InterVariable.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: Inter;
|
||||
font-style: italic;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url("InterVariable-Italic.woff2") format("woff2");
|
||||
}
|
BIN
public/wheel-loop-dark.mp4
Normal file
BIN
public/wheel-loop.mp4
Normal file
38
sign-win.js
Normal file
@ -0,0 +1,38 @@
|
||||
// From https://github.com/OpenBuilds/OpenBuilds-CONTROL/blob/4800540ffaa517925fc2cff26670809efa341ffe/signWin.js
|
||||
const { execSync } = require('node:child_process')
|
||||
|
||||
exports.default = async (configuration) => {
|
||||
if (!process.env.SM_API_KEY) {
|
||||
console.error(
|
||||
'Signing using signWin.js script: failed: SM_API_KEY ENV VAR NOT FOUND'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!process.env.WINDOWS_CERTIFICATE_THUMBPRINT) {
|
||||
console.error(
|
||||
'Signing using signWin.js script: failed: FINGERPRINT ENV VAR NOT FOUND'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!configuration.path) {
|
||||
throw new Error(
|
||||
`Signing using signWin.js script: failed: TARGET PATH NOT FOUND`
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`smctl sign --fingerprint="${
|
||||
process.env.WINDOWS_CERTIFICATE_THUMBPRINT
|
||||
}" --input "${String(configuration.path)}"`,
|
||||
{
|
||||
stdio: 'inherit',
|
||||
}
|
||||
)
|
||||
console.log('Signing using signWin.js script: successful')
|
||||
} catch (error) {
|
||||
console.error('Signing using signWin.js script: failed:', error)
|
||||
}
|
||||
}
|
50
src/App.tsx
@ -1,12 +1,8 @@
|
||||
import { MouseEventHandler, useEffect, useMemo, useRef } from 'react'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||
import { Stream } from './components/Stream'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { throttle } from './lib/utils'
|
||||
import { AppHeader } from './components/AppHeader'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { getNormalisedCoordinates } from './lib/utils'
|
||||
import { useLoaderData, useNavigate } from 'react-router-dom'
|
||||
import { type IndexLoaderData } from 'lib/types'
|
||||
import { PATHS } from 'lib/paths'
|
||||
@ -14,7 +10,6 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { useLspContext } from 'components/LspProvider'
|
||||
@ -44,7 +39,6 @@ export function App() {
|
||||
}, [projectName, projectPath])
|
||||
|
||||
useHotKeyListener()
|
||||
const { context, state } = useModelingContext()
|
||||
|
||||
const { auth, settings } = useSettingsAuthContext()
|
||||
const token = auth?.context?.token
|
||||
@ -73,52 +67,14 @@ export function App() {
|
||||
(p) => p === onboardingStatus.current
|
||||
)
|
||||
? 'opacity-20'
|
||||
: context.store?.didDragInStream
|
||||
? 'opacity-40'
|
||||
: ''
|
||||
|
||||
useEngineConnectionSubscriptions()
|
||||
|
||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||
engineCommandManager.sendSceneCommand(message)
|
||||
}, 1000 / 15)
|
||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
if (state.matches('Sketch')) {
|
||||
return
|
||||
}
|
||||
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
el: e.currentTarget,
|
||||
...context.store?.streamDimensions,
|
||||
})
|
||||
|
||||
const newCmdId = uuidv4()
|
||||
if (state.matches('idle.showPlanes')) return
|
||||
if (context.store?.buttonDownInStream !== undefined) return
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-full flex flex-col"
|
||||
onMouseMove={handleMouseMove}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="relative h-full flex flex-col" ref={ref}>
|
||||
<AppHeader
|
||||
className={
|
||||
'transition-opacity transition-duration-75 ' +
|
||||
paneOpacity +
|
||||
(context.store?.buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
className={'transition-opacity transition-duration-75 ' + paneOpacity}
|
||||
project={{ project, file }}
|
||||
enableMenu={true}
|
||||
/>
|
||||
|