Compare commits

..

14 Commits

Author SHA1 Message Date
55a6155400 WIP: Start using the recursion crate 2024-08-19 11:09:37 -04:00
d41972ee4d Add recursion dependency 2024-08-19 11:09:37 -04:00
2d6c8cfe32 Add electron test for settings being set on home level and overridden by project level (#3527)
Add a test for settings being set on home level and overridden by project level
2024-08-19 10:23:43 -04:00
37c6730c02 Fix CPU-driven churn once Text-to-CAD toast appears in the app (#3523)
* Dispose of requestAnimationFrame loop when component unmounts

* Only run requestAnimationFrame loop when mouse is on canvas

* Better animation loop disposal on canvas mouseout

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Text-to-cad test flakiness

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Re-run CI

* Remove arbitrary timeout which may cause us to miss the toast on a fast-running test

* Remove a couple more arbitrary timeouts in text-to-cad tests

* Remove all the arbitrary 5s awaits from these tests

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-19 09:38:47 -04:00
337f828aa4 Franknoirot/electron network regression test (#3525)
* Only show the network health indicator when in the modeling view

* Add test to confirm network health indicator behavior
2024-08-19 08:58:24 -04:00
d845e7c38d playwright robustness: "can do many at once and get many prompts back, and interact with many" (#3524)
* playwright robustness

* try fix the close button
2024-08-19 22:38:17 +10:00
7f50294936 Fix up little differences in file/dir creation logic for electron (#3498)
* Fix up little differences in file/dir creation logic for electron

* Fix typo

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"

This reverts commit 8e7212f5da.

* Text-to-cad test flakiness

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-19 08:08:07 -04:00
73bbd3f5b7 Revert "Add a test for settings being set on home level and overridden by project level"
This reverts commit 295b98c021.
2024-08-19 14:00:28 +02:00
295b98c021 Add a test for settings being set on home level and overridden by project level 2024-08-19 13:56:30 +02:00
2e24137863 Electron export test #3376 (#3512)
* electron export test #3376

* typo

* increase timeout
2024-08-19 16:29:44 +10:00
5e694961e8 get unit tests running again (#3519) 2024-08-19 14:47:27 +10:00
a1ef4ff86f playwright order in github actions (#3509)
playwright matrix tweak
2024-08-19 14:01:28 +10:00
ccd31b7d6d rename for #3367 (#3508) 2024-08-19 11:55:18 +10:00
b5ddbb7fa7 electron delete project tests (#3507)
add delete projcet tests #3365
2024-08-19 11:49:03 +10:00
20 changed files with 967 additions and 408 deletions

View File

@ -10,6 +10,11 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
actions: read
jobs:
check-format:
runs-on: 'ubuntu-22.04'
@ -56,7 +61,7 @@ jobs:
build-test-web:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -74,5 +79,37 @@ jobs:
- run: yarn build:wasm
- run: yarn simpleserver:ci
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
- run: yarn test:nowatch
- name: Install Chromium Browser
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: yarn playwright install chromium --with-deps
- name: run unit tests
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: yarn test:nowatch
env:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: check for changes
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
id: git-check
run: |
git add src/lang/std/artifactMapGraphs
if git status src/lang/std/artifactMapGraphs | grep -q "Changes to be committed"
then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes, if any
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' && steps.git-check.outputs.modified == 'true' }}
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
# TODO when webkit works on ubuntu remove the os part of the commit message
git commit -am "Look at this (photo)Graph *in the voice of Nickelback*" || true
git push
git push origin ${{ github.head_ref }}

View File

@ -1,7 +1,7 @@
name: Playwright Tests
on:
push:
branches: [ main, ryanrosello-og/troubleshoot-turn-on-macos ]
branches: [ main ]
pull_request:
branches: [ main ]
@ -34,14 +34,13 @@ jobs:
- 'src/wasm-lib/**'
playwright-chrome:
timeout-minutes: ${{ matrix.os == 'macos-14' && 60 || 30 }}
timeout-minutes: ${{ matrix.os == 'macos-14' && 60 || 40 }}
strategy:
fail-fast: false
matrix:
shardIndex: [1]
shardTotal: [1]
os: [macos-14]
# os: [ubuntu-latest, windows-latest, macos-14]
os: [ubuntu-latest, windows-latest]
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
runs-on: ${{ matrix.os }}
needs: check-rust-changes
steps:
@ -56,19 +55,15 @@ jobs:
- name: Install dependencies
shell: bash
run: yarn
# - name: Cache Playwright Browsers
# uses: actions/cache@v4
# with:
# path: |
# ~/.cache/ms-playwright/
# key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
- name: Cache Playwright Browsers
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright/
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
- name: Install Playwright Browsers
shell: bash
run: yarn playwright install --with-deps
# - name: install chrome from the cask macos
# if: ${{ startsWith(matrix.os, 'macos') }}
# shell: bash
# run: |
# brew install --cask google-chrome
- name: Download Wasm Cache
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
@ -97,28 +92,28 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
# - name: install good sed
# if: ${{ startsWith(matrix.os, 'macos') }}
# shell: bash
# run: |
# brew install gnu-sed
# echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
# - name: Install vector
# shell: bash
# if: ${{ !startsWith(matrix.os, 'windows') }}
# run: |
# curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
# chmod +x /tmp/vector.sh
# /tmp/vector.sh -y -no-modify-path
# mkdir -p /tmp/vector
# cp .github/workflows/vector.toml /tmp/vector.toml
# sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
# sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
# sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
# sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
# sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
# cat /tmp/vector.toml
# ${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- name: install good sed
if: ${{ startsWith(matrix.os, 'macos') }}
shell: bash
run: |
brew install gnu-sed
echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
- name: Install vector
shell: bash
if: ${{ !startsWith(matrix.os, 'windows') }}
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
shell: bash
@ -130,112 +125,111 @@ jobs:
- name: build web
run: yarn build:local
shell: bash
# - name: Run playwright/chrome snapshots
# shell: bash
# run: |
# yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
# env:
# CI: true
# NODE_ENV: development
# VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
# VITE_KC_SKIP_AUTH: true
# token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
# snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
# - uses: actions/upload-artifact@v4
# if: ${{ !cancelled() && (success() || failure()) }}
# with:
# name: playwright-report-${{matrix.os}}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
# path: playwright-report/
# retention-days: 30
# overwrite: true
# - name: Clean up test-results
# if: ${{ !cancelled() && (success() || failure()) }}
# continue-on-error: true
# run: rm -r test-results
# - name: check for changes
# shell: bash
# id: git-check
# run: |
# git add .
# if git status | grep -q "Changes to be committed"
# then echo "modified=true" >> $GITHUB_OUTPUT
# else echo "modified=false" >> $GITHUB_OUTPUT
# fi
# - name: Commit changes, if any
# if: steps.git-check.outputs.modified == 'true'
# shell: bash
# run: |
# git add .
# git config --local user.email "github-actions[bot]@users.noreply.github.com"
# git config --local user.name "github-actions[bot]"
# git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
# git fetch origin
# echo ${{ github.head_ref }}
# git checkout ${{ github.head_ref }}
# git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
# git push
# git push origin ${{ github.head_ref }}
# # only upload artifacts if there's actually changes
# - uses: actions/upload-artifact@v4
# if: steps.git-check.outputs.modified == 'true'
# with:
# 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-${{matrix.os}}-${{ matrix.shardIndex }}-${{ github.sha }}
# path: test-results/
- name: Run ubuntu/chrome snapshots
shell: bash
run: |
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
CI: true
NODE_ENV: development
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
with:
name: playwright-report-ubuntu-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true
- name: Clean up test-results
if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true
run: rm -r test-results
- name: check for changes
shell: bash
id: git-check
run: |
git add .
if git status | grep -q "Changes to be committed"
then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes, if any
if: steps.git-check.outputs.modified == 'true'
shell: bash
run: |
git add .
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
git push
git push origin ${{ github.head_ref }}
# only upload artifacts if there's actually changes
- uses: actions/upload-artifact@v4
if: steps.git-check.outputs.modified == 'true'
with:
name: playwright-report-ubuntu-${{ 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 }}
path: test-results/
- name: Run playwright/chrome flow (with retries)
id: retry
if: ${{ !cancelled() && (success() || failure()) }}
shell: bash
run: |
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep="@focus" --grep-invert="@snapshot|@electron" || true
# if [[ ! -f "test-results/.last-run.json" ]]; then
# # if no last run artifact, than run plawright normally
# echo "run playwright normally"
# yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert="@snapshot|@electron" || true
# # # send to axiom
# node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
# fi
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert="@snapshot|@electron" || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
# retry=1
# max_retrys=4
retry=1
max_retrys=4
# # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
# while [[ $retry -le $max_retrys ]]; do
# if [[ -f "test-results/.last-run.json" ]]; then
# failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
# if [[ $failed_tests -gt 0 ]]; then
# echo "retried=true" >>$GITHUB_OUTPUT
# echo "run playwright with last failed tests and retry $retry"
# yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --last-failed --grep-invert="@snapshot|@electron" || true
# # send to axiom
# node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
# retry=$((retry + 1))
# else
# echo "retried=false" >>$GITHUB_OUTPUT
# exit 0
# fi
# else
# echo "retried=false" >>$GITHUB_OUTPUT
# exit 0
# fi
# done
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --last-failed --grep-invert="@snapshot|@electron" || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
done
# echo "retried=false" >>$GITHUB_OUTPUT
echo "retried=false" >>$GITHUB_OUTPUT
# if [[ -f "test-results/.last-run.json" ]]; then
# failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
# if [[ $failed_tests -gt 0 ]]; then
# # if it still fails after 3 retrys, then fail the job
# exit 1
# fi
# fi
# exit 0
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
# if it still fails after 3 retrys, then fail the job
exit 1
fi
fi
exit 0
env:
CI: true
NODE_ENV: development
@ -250,193 +244,193 @@ jobs:
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{matrix.os}}-${{ matrix.shardIndex }}-${{ github.sha }}
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{matrix.os}}-${{ matrix.shardIndex }}-${{ github.sha }}
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true
# playwright-electron:
# strategy:
# fail-fast: false
# matrix:
# os: [ubuntu-latest, windows-latest, macos-14]
# timeout-minutes: 30
# runs-on: ${{ matrix.os }}
# needs: check-rust-changes
# steps:
# - name: Tune GitHub-hosted runner network
# uses: smorimoto/tune-github-hosted-runner-network@v1
# - uses: actions/checkout@v4
# - uses: actions/setup-node@v4
# with:
# node-version-file: '.nvmrc'
# cache: 'yarn'
# - uses: KittyCAD/action-install-cli@main
# - name: Install dependencies
# shell: bash
# run: yarn
# - name: Cache Playwright Browsers
# uses: actions/cache@v4
# with:
# path: |
# ~/.cache/ms-playwright/
# key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
# - name: Install Playwright Browsers
# shell: bash
# run: yarn playwright install chromium --with-deps
# - name: Download Wasm Cache
# id: download-wasm
# if: needs.check-rust-changes.outputs.rust-changed == 'false'
# uses: dawidd6/action-download-artifact@v6
# continue-on-error: true
# with:
# github_token: ${{secrets.GITHUB_TOKEN}}
# name: wasm-bundle
# workflow: build-and-store-wasm.yml
# branch: main
# path: src/wasm-lib/pkg
# - name: copy wasm blob
# if: needs.check-rust-changes.outputs.rust-changed == 'false'
# shell: bash
# run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
# continue-on-error: true
# - name: Setup Rust
# uses: dtolnay/rust-toolchain@stable
# - name: Cache Wasm (because rust diff)
# if: needs.check-rust-changes.outputs.rust-changed == 'true'
# uses: Swatinem/rust-cache@v2
# with:
# workspaces: './src/wasm-lib'
# - name: OR Cache Wasm (because wasm cache failed)
# if: steps.download-wasm.outcome == 'failure'
# uses: Swatinem/rust-cache@v2
# with:
# workspaces: './src/wasm-lib'
# - name: install good sed
# if: ${{ startsWith(matrix.os, 'macos') }}
# shell: bash
# run: |
# brew install gnu-sed
# echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
# - name: Install vector
# if: ${{ !startsWith(matrix.os, 'windows') }}
# shell: bash
# run: |
# curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
# chmod +x /tmp/vector.sh
# /tmp/vector.sh -y -no-modify-path
# mkdir -p /tmp/vector
# cp .github/workflows/vector.toml /tmp/vector.toml
# sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
# sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
# sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
# sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
# sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
# cat /tmp/vector.toml
# ${HOME}/.vector/bin/vector --config /tmp/vector.toml &
# - name: Build Wasm (because rust diff)
# if: needs.check-rust-changes.outputs.rust-changed == 'true'
# shell: bash
# run: yarn build:wasm
# - name: OR Build Wasm (because wasm cache failed)
# if: steps.download-wasm.outcome == 'failure'
# shell: bash
# run: yarn build:wasm
# - name: build electron
# shell: bash
# run: yarn electron:package
# - uses: actions/download-artifact@v4
# if: ${{ !cancelled() && (success() || failure()) }}
# continue-on-error: true
# with:
# name: test-results-${{matrix.os}}-${{ github.sha }}
# path: test-results/
# - name: Run electron tests (with retries)
# id: retry
# if: ${{ !cancelled() && (success() || failure()) }}
# shell: bash
# run: |
# if [[ ! -f "test-results/.last-run.json" ]]; then
# # if no last run artifact, than run plawright normally
# echo "run playwright normally"
# 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
# else
# yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
# fi
# # # send to axiom
# node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
# fi
playwright-electron:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-14]
timeout-minutes: 30
runs-on: ${{ matrix.os }}
needs: check-rust-changes
steps:
- name: Tune GitHub-hosted runner network
uses: smorimoto/tune-github-hosted-runner-network@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- uses: KittyCAD/action-install-cli@main
- name: Install dependencies
shell: bash
run: yarn
- name: Cache Playwright Browsers
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright/
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
- name: Install Playwright Browsers
shell: bash
run: yarn playwright install chromium --with-deps
- name: Download Wasm Cache
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v6
continue-on-error: true
with:
github_token: ${{secrets.GITHUB_TOKEN}}
name: wasm-bundle
workflow: build-and-store-wasm.yml
branch: main
path: src/wasm-lib/pkg
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
shell: bash
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: OR Cache Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: install good sed
if: ${{ startsWith(matrix.os, 'macos') }}
shell: bash
run: |
brew install gnu-sed
echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
- name: Install vector
if: ${{ !startsWith(matrix.os, 'windows') }}
shell: bash
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
shell: bash
run: yarn build:wasm
- name: OR Build Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
shell: bash
run: yarn build:wasm
- name: build electron
shell: bash
run: yarn electron:package
- uses: actions/download-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true
with:
name: test-results-ubuntu-${{ github.sha }}
path: test-results/
- name: Run electron tests (with retries)
id: retry
if: ${{ !cancelled() && (success() || failure()) }}
shell: bash
run: |
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
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
else
yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
fi
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
# retry=1
# max_retrys=2
retry=1
max_retrys=2
# # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
# while [[ $retry -le $max_retrys ]]; do
# if [[ -f "test-results/.last-run.json" ]]; then
# failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
# if [[ $failed_tests -gt 0 ]]; then
# echo "retried=true" >>$GITHUB_OUTPUT
# echo "run playwright with last failed tests and retry $retry"
# if [[ "$IS_UBUNTU" == "true" ]]; then
# xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
# else
# yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
# fi
# # send to axiom
# node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
# retry=$((retry + 1))
# else
# echo "retried=false" >>$GITHUB_OUTPUT
# exit 0
# fi
# else
# echo "retried=false" >>$GITHUB_OUTPUT
# exit 0
# fi
# done
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
if [[ "$IS_UBUNTU" == "true" ]]; then
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
else
yarn playwright test --config=playwright.electron.config.ts --grep=@electron || true
fi
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
done
# echo "retried=false" >>$GITHUB_OUTPUT
echo "retried=false" >>$GITHUB_OUTPUT
# if [[ -f "test-results/.last-run.json" ]]; then
# failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
# if [[ $failed_tests -gt 0 ]]; then
# # if it still fails after 3 retrys, then fail the job
# exit 1
# fi
# fi
# exit 0
# env:
# CI: true
# NODE_ENV: development
# VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
# VITE_KC_SKIP_AUTH: true
# IS_UBUNTU: ${{ startsWith(matrix.os, 'ubuntu') && 'true' || 'false' }}
# #DEBUG: 'pw:browser*'
# - name: send to axiom
# if: ${{ !cancelled() && (success() || failure()) && !startsWith(matrix.os, 'windows') }}
# shell: bash
# run: |
# node playwrightProcess.mjs | tee /tmp/github-actions.log
# - uses: actions/upload-artifact@v4
# if: ${{ !cancelled() && (success() || failure()) }}
# with:
# name: test-results-electron-${{ github.sha }}
# path: test-results/
# retention-days: 30
# overwrite: true
# - uses: actions/upload-artifact@v4
# if: ${{ !cancelled() && (success() || failure()) }}
# with:
# name: playwright-report-electron-${{ github.sha }}
# path: playwright-report/
# retention-days: 30
# overwrite: true
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
# if it still fails after 3 retrys, then fail the job
exit 1
fi
fi
exit 0
env:
CI: true
NODE_ENV: development
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true
IS_UBUNTU: ${{ startsWith(matrix.os, 'ubuntu') && 'true' || 'false' }}
#DEBUG: 'pw:browser*'
- name: send to axiom
if: ${{ !cancelled() && (success() || failure()) && !startsWith(matrix.os, 'windows') }}
shell: bash
run: |
node playwrightProcess.mjs | tee /tmp/github-actions.log
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
with:
name: test-results-electron-${{ github.sha }}
path: test-results/
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
with:
name: playwright-report-electron-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true

View File

@ -131,7 +131,7 @@ test.describe('Onboarding tests', () => {
await expect(page.url()).not.toContain('onboarding')
})
test('Onboarding redirects and code updating @focus', async ({ page }) => {
test('Onboarding redirects and code updating', async ({ page }) => {
const u = await getUtils(page)
// Override beforeEach test setup

View File

@ -1,5 +1,11 @@
import { test, expect } from '@playwright/test'
import { getUtils, setupElectron, tearDown } from './test-utils'
import {
doExport,
getUtils,
Paths,
setupElectron,
tearDown,
} from './test-utils'
import fsp from 'fs/promises'
import fs from 'fs'
import { join } from 'path'
@ -8,6 +14,94 @@ test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test(
'Can export from electron app',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/bracket/main.kcl`
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
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 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
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,
})
// 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]), {
timeout: 10_000,
})
.toBeLessThan(10)
})
const exportLocations: Array<Paths> = []
await test.step('export the model as a glTF', async () => {
exportLocations.push(
await doExport(
{
type: 'gltf',
storage: 'embedded',
presentation: 'pretty',
},
page,
true
)
)
})
await test.step('Check the export size', async () => {
await expect
.poll(
async () => {
try {
const outputGltf = await fsp.readFile('output.gltf')
return outputGltf.byteLength
} catch (e) {
return 0
}
},
{ timeout: 15_000 }
)
.toBe(477327)
// clean up output.gltf
await fsp.rm('output.gltf')
})
await electronApp.close()
}
)
test(
'Rename and delete projects, also spam arrow keys when renaming',
{ tag: '@electron' },
@ -66,10 +160,10 @@ test(
await page.waitForTimeout(100)
// type "updated project name"
await page.keyboard.press('Backspace')
await page.keyboard.type('updated project name')
// spam arrow keys to make sure it doesn't impact the text
for (let i = 0; i < 10; i++) {
await page.keyboard.press('ArrowRight')
}
@ -295,6 +389,101 @@ test.fixme(
await electronApp.close()
}
)
test(
'Deleting projects, can delete individual project, can still create projects after deleting all',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
})
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
const createProjectAndRenameIt = async (name: string) =>
test.step(`Create and rename project ${name}`, async () => {
await page.getByRole('button', { name: 'New project' }).click()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
await expect(page.getByText(`project-000`)).toBeVisible()
await page.getByText(`project-000`).hover()
await page.getByText(`project-000`).focus()
await page.getByLabel('sketch').first().click()
await page.waitForTimeout(100)
// type "updated project name"
await page.keyboard.press('Backspace')
await page.keyboard.type(name)
await page.getByLabel('checkmark').last().click()
})
// we need to create the folders so that the order is correct
// creating them ahead of time with fs tools means they all have the same timestamp
await createProjectAndRenameIt('router-template-slate')
// await createProjectAndRenameIt('focusrite_scarlett_mounting_braket')
await createProjectAndRenameIt('bracket')
await createProjectAndRenameIt('lego')
await test.step('delete the middle project, i.e. the bracket project', async () => {
const project = page.getByText('bracket')
await project.hover()
await project.focus()
await page
.locator('[data-edit-buttons-for="bracket"]')
.getByLabel('trash')
.click()
await expect(page.getByText('This will permanently delete')).toBeVisible()
await page.getByTestId('delete-confirmation').click()
await expect(page.getByText('Successfully deleted')).toBeVisible()
await expect(page.getByText('Successfully deleted')).not.toBeVisible()
await expect(page.getByText('bracket')).not.toBeVisible()
})
await test.step('Now that the middle project is deleted, check the other projects are still there', async () => {
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('lego')).toBeVisible()
})
await test.step('delete other two projects', async () => {
await page
.locator('[data-edit-buttons-for="router-template-slate"]')
.getByLabel('trash')
.click()
await page.getByTestId('delete-confirmation').click()
await page
.locator('[data-edit-buttons-for="lego"]')
.getByLabel('trash')
.click()
await page.getByTestId('delete-confirmation').click()
})
await test.step('Check that the home page is empty', async () => {
await expect(page.getByText('No Projects found')).toBeVisible()
})
await test.step('Check we can still create a project', async () => {
await page.getByRole('button', { name: 'New project' }).click()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
await expect(page.getByText('project-000')).toBeVisible()
})
await electronApp.close()
}
)
test(
'Can sort projects on home page',
{ tag: '@electron' },
@ -502,7 +691,7 @@ const extrude001 = extrude(200, sketch001)`)
)
test(
'Check you can go home with two different methods, and that switching between projects does not harm the stream',
'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({

View File

@ -1,6 +1,6 @@
import { test, expect, Page } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import * as fsp from 'fs/promises'
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
import { bracket } from 'lib/exampleKcl'
@ -415,6 +415,52 @@ const sketch001 = startSketchAt([-0, -0])
await expect(successToastMessage).toBeVisible()
})
})
test(
`Network health indicator only appears in modeling view`,
{ tag: '@electron' },
async ({ browserName: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/bracket/main.kcl`
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
// Locators
const projectsHeading = page.getByRole('heading', {
name: 'Your projects',
})
const projectLink = page.getByRole('link', { name: 'bracket' })
const networkHealthIndicator = page.getByTestId('network-toggle')
await test.step('Check the home page', async () => {
await expect(projectsHeading).toBeVisible()
await expect(projectLink).toBeVisible()
await expect(networkHealthIndicator).not.toBeVisible()
})
await test.step('Open the project', async () => {
await projectLink.click()
})
await test.step('Check the modeling view', async () => {
await expect(networkHealthIndicator).toBeVisible()
await expect(networkHealthIndicator).toContainText('Problem')
await u.waitForPageLoad()
await expect(networkHealthIndicator).toContainText('Connected')
})
await electronApp.close()
}
)
})
async function clickExportButton(page: Page) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -538,14 +538,19 @@ export interface Paths {
export const doExport = async (
output: Models['OutputFormat_type'],
page: Page
page: Page,
isElectron = false
): Promise<Paths> => {
await page.getByRole('button', { name: APP_NAME }).click()
const exportMenuButton = page.getByRole('button', {
name: 'Export current part',
})
await expect(exportMenuButton).toBeVisible()
await exportMenuButton.click()
if (!isElectron) {
await page.getByRole('button', { name: APP_NAME }).click()
const exportMenuButton = page.getByRole('button', {
name: 'Export current part',
})
await expect(exportMenuButton).toBeVisible()
await exportMenuButton.click()
} else {
await page.getByTestId('export-pane-button').click()
}
await expect(page.getByTestId('command-bar')).toBeVisible()
// Go through export via command bar
@ -572,13 +577,21 @@ export const doExport = async (
const [downloadPromise1, downloadResolve1] = getPromiseAndResolve()
let downloadCnt = 0
page.on('download', async (download) => {
if (downloadCnt === 0) {
downloadResolve1(download)
}
downloadCnt++
})
if (!isElectron)
page.on('download', async (download) => {
if (downloadCnt === 0) {
downloadResolve1(download)
}
downloadCnt++
})
await page.getByRole('button', { name: 'Submit command' }).click()
if (isElectron) {
return {
modelPath: '',
imagePath: '',
outputType: output.type,
}
}
// Handle download
const download = await downloadPromise1

View File

@ -1,9 +1,10 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import * as fsp from 'fs/promises'
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates'
import * as TOML from '@iarna/toml'
import { APP_NAME } from 'lib/constants'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
@ -187,4 +188,74 @@ test.describe('Testing settings', () => {
await expect(themeColorSetting).toHaveValue(settingValues.default)
})
})
test(
`Project settings override user settings on desktop`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/bracket/main.kcl`
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
// Selectors and constants
const userThemeColor = '120'
const projectThemeColor = '50'
const settingsOpenButton = page.getByRole('link', {
name: 'settings Settings',
})
const themeColorSetting = page.locator('#themeColor').getByRole('slider')
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
const userSettingsTab = page.getByRole('radio', { name: 'User' })
const settingsCloseButton = page.getByTestId('settings-close-button')
const projectLink = page.getByText('bracket')
const logoLink = page.getByTestId('app-logo')
// Open the app and set the user theme color
await test.step('Set user theme color on home', async () => {
await expect(settingsOpenButton).toBeVisible()
await settingsOpenButton.click()
// The user tab should be selected by default on home
await expect(userSettingsTab).toBeChecked()
await themeColorSetting.fill(userThemeColor)
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
await settingsCloseButton.click()
})
await test.step('Set project theme color', async () => {
// Open the project
await projectLink.click()
await settingsOpenButton.click()
// The project tab should be selected by default within a project
await expect(projectSettingsTab).toBeChecked()
await themeColorSetting.fill(projectThemeColor)
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
})
await test.step('Refresh the application and see project setting applied', async () => {
await page.reload()
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
await settingsCloseButton.click()
})
await test.step(`Navigate back to the home view and see user setting applied`, async () => {
await logoLink.click()
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
})
await electronApp.close()
}
)
})

View File

@ -31,15 +31,13 @@ test.describe('Text-to-CAD tests', () => {
)
await expect(submittingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
const generatingToastMessage = page.getByText(
`Generating parametric model...`
)
await expect(generatingToastMessage).toBeVisible()
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
const successToastMessage = page.getByText(`Text-to-CAD successful`)
await expect(successToastMessage).toBeVisible()
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
await expect(page.getByText('Copied')).not.toBeVisible()
@ -101,15 +99,13 @@ test.describe('Text-to-CAD tests', () => {
)
await expect(submittingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
const generatingToastMessage = page.getByText(
`Generating parametric model...`
)
await expect(generatingToastMessage).toBeVisible()
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
const successToastMessage = page.getByText(`Text-to-CAD successful`)
await expect(successToastMessage).toBeVisible()
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
await expect(page.getByText('Copied')).not.toBeVisible()
@ -121,13 +117,12 @@ test.describe('Text-to-CAD tests', () => {
// Find the toast.
// Look out for the toast message
await expect(submittingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
await expect(generatingToastMessage).toBeVisible()
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
// Expect 2 success toasts.
await expect(successToastMessage).toHaveCount(2)
await expect(successToastMessage).toHaveCount(2, {
timeout: 15000,
})
await expect(page.getByText('a 2x4 lego')).toBeVisible()
await expect(page.getByText('a 2x6 lego')).toBeVisible()
})
@ -150,15 +145,13 @@ test.describe('Text-to-CAD tests', () => {
)
await expect(submittingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
const generatingToastMessage = page.getByText(
`Generating parametric model...`
)
await expect(generatingToastMessage).toBeVisible()
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
const successToastMessage = page.getByText(`Text-to-CAD successful`)
await expect(successToastMessage).toBeVisible()
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
// Hit copy to clipboard.
const rejectButton = page.getByRole('button', { name: 'Reject' })
@ -319,11 +312,9 @@ test.describe('Text-to-CAD tests', () => {
// Look out for the toast message
await expect(submittingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
await expect(generatingToastMessage).toBeVisible()
await expect(successToastMessage).toBeVisible()
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
})
test('sending a bad prompt fails, can ignore toast, can start over from command bar', async ({
@ -353,7 +344,7 @@ test.describe('Text-to-CAD tests', () => {
const prompt = page.getByText('Prompt')
await expect(prompt.first()).toBeVisible()
const badPrompt = 'akjsndladf lajbhflauweyfa;wieufjn---4;'
const badPrompt = 'akjsndladflajbhflauweyf15;'
// Type the prompt.
await page.keyboard.type(badPrompt)
@ -391,11 +382,9 @@ test.describe('Text-to-CAD tests', () => {
// Look out for the toast message
await expect(submittingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
await expect(generatingToastMessage).toBeVisible()
await expect(successToastMessage).toBeVisible()
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
await expect(page.getByText('Copied')).not.toBeVisible()
@ -448,16 +437,13 @@ test.describe('Text-to-CAD tests', () => {
)
await expect(submittingToastMessage).toBeVisible()
await page.waitForTimeout(1000)
const generatingToastMessage = page.getByText(
`Generating parametric model...`
)
await expect(generatingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
const successToastMessage = page.getByText(`Text-to-CAD successful`)
await expect(successToastMessage).toBeVisible()
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
await expect(page.getByText(promptWithNewline)).toBeVisible()
})
@ -465,6 +451,8 @@ test.describe('Text-to-CAD tests', () => {
test('can do many at once and get many prompts back, and interact with many', async ({
page,
}) => {
// Let this test run longer since we've seen it timeout.
test.setTimeout(180_000)
// skip on windows
test.skip(
process.platform === 'win32',
@ -493,11 +481,13 @@ test.describe('Text-to-CAD tests', () => {
const generatingToastMessage = page.getByText(
`Generating parametric model...`
)
await expect(generatingToastMessage.first()).toBeVisible({ timeout: 10000 })
await expect(generatingToastMessage.first()).toBeVisible({
timeout: 10_000,
})
const successToastMessage = page.getByText(`Text-to-CAD successful`)
// We should have three success toasts.
await expect(successToastMessage).toHaveCount(3, { timeout: 15000 })
await expect(successToastMessage).toHaveCount(3, { timeout: 25_000 })
await expect(page.getByText('Copied')).not.toBeVisible()
@ -539,12 +529,7 @@ test.describe('Text-to-CAD tests', () => {
await expect(page.locator('.cm-content')).toContainText(`2x8`)
// Find the toast close button.
const closeButton = page
.getByRole('status')
.locator('div')
.filter({ hasText: 'Text-to-CAD successfulPrompt' })
.first()
.getByRole('button', { name: 'Close' })
const closeButton = page.locator('[data-negative-button="close"]').first()
await expect(closeButton).toBeVisible()
await closeButton.click()
@ -697,7 +682,7 @@ async function sendPromptFromCommandBar(page: Page, promptStr: string) {
// Type the prompt.
await page.keyboard.type(promptStr)
await page.waitForTimeout(1000)
await page.waitForTimeout(200)
await page.keyboard.press('Enter')
})
}

View File

@ -42,9 +42,6 @@ export default defineConfig({
/* Chromium is the only one with these permission types */
permissions: ['clipboard-write', 'clipboard-read'],
},
launchOptions: {
args: process.env.CI ? ['--headless', '--enable-gpu'] : [],
},
}, // or 'chrome-beta'
},
{

View File

@ -49,7 +49,7 @@ export const FileMachineProvider = ({
if (event.data && 'name' in event.data) {
commandBarSend({ type: 'Close' })
navigate(
`${PATHS.FILE}/${encodeURIComponent(
`..${PATHS.FILE}/${encodeURIComponent(
context.selectedDirectory +
window.electron.path.sep +
event.data.name
@ -61,7 +61,7 @@ export const FileMachineProvider = ({
event.data.path.endsWith(FILE_EXT)
) {
// Don't navigate to newly created directories
navigate(`${PATHS.FILE}/${encodeURIComponent(event.data.path)}`)
navigate(`..${PATHS.FILE}/${encodeURIComponent(event.data.path)}`)
}
},
addFileToRenamingQueue: assign({
@ -100,7 +100,7 @@ export const FileMachineProvider = ({
let createdPath: string
if (event.data.makeDir) {
let { name, path } = await getNextDirName({
let { name, path } = getNextDirName({
entryName: createdName,
baseDir: context.selectedDirectory.path,
})
@ -108,16 +108,13 @@ export const FileMachineProvider = ({
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = await getNextFileName({
const { name, path } = getNextFileName({
entryName: createdName,
baseDir: context.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
if (event.data.content) {
await window.electron.writeFile(createdPath, event.data.content)
}
await window.electron.writeFile(createdPath, event.data.content ?? '')
}
return {
@ -130,7 +127,7 @@ export const FileMachineProvider = ({
let createdPath: string
if (event.data.makeDir) {
let { name, path } = await getNextDirName({
let { name, path } = getNextDirName({
entryName: createdName,
baseDir: context.selectedDirectory.path,
})
@ -138,16 +135,13 @@ export const FileMachineProvider = ({
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = await getNextFileName({
const { name, path } = getNextFileName({
entryName: createdName,
baseDir: context.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
if (event.data.content) {
await window.electron.writeFile(createdPath, '')
}
await window.electron.writeFile(createdPath, event.data.content ?? '')
}
return {
@ -180,13 +174,13 @@ export const FileMachineProvider = ({
const currentFilePath = window.electron.path.join(file.path, file.name)
if (oldPath === currentFilePath && project?.path) {
// If we just renamed the current file, navigate to the new path
navigate(PATHS.FILE + '/' + encodeURIComponent(newPath))
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
} else if (file?.path.includes(oldPath)) {
// If we just renamed a directory that the current file is in, navigate to the new path
navigate(
PATHS.FILE +
'/' +
encodeURIComponent(file.path.replace(oldPath, newDirPath))
`..${PATHS.FILE}/${encodeURIComponent(
file.path.replace(oldPath, newDirPath)
)}`
)
}
@ -221,7 +215,7 @@ export const FileMachineProvider = ({
file?.path.includes(event.data.path)) &&
project?.path
) {
navigate(PATHS.FILE + '/' + encodeURIComponent(project.path))
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
}
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${

View File

@ -107,7 +107,9 @@ export function LowerRightControls({
</Tooltip>
</Link>
<NetworkMachineIndicator className={linkOverrideClassName} />
<NetworkHealthIndicator />
{!location.pathname.startsWith(PATHS.HOME) && (
<NetworkHealthIndicator />
)}
<HelpMenu />
</menu>
</section>

View File

@ -120,7 +120,10 @@ function ProjectCard({
</div>
</Link>
{!isEditing && (
<div className="absolute z-10 flex items-center gap-1 opacity-0 bottom-2 right-2 group-hover:opacity-100 group-focus-within:opacity-100">
<div
className="absolute z-10 flex items-center gap-1 opacity-0 bottom-2 right-2 group-hover:opacity-100 group-focus-within:opacity-100"
data-edit-buttons-for={project.name?.replace(FILE_EXT, '')}
>
<ActionButton
Element="button"
iconStart={{

View File

@ -5,7 +5,7 @@ import { isDesktop } from 'lib/isDesktop'
import { PATHS } from 'lib/paths'
import toast from 'react-hot-toast'
import { TextToCad_type } from '@kittycad/lib/dist/types/src/models'
import { useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
Box3,
Color,
@ -121,10 +121,40 @@ export function ToastTextToCadSuccess({
}) {
const wrapperRef = useRef<HTMLDivElement | null>(null)
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const animationRequestRef = useRef<number>()
const [hasCopied, setHasCopied] = useState(false)
const [showCopiedUi, setShowCopiedUi] = useState(false)
const modelId = data.id
const animate = useCallback(
({
renderer,
scene,
camera,
controls,
isFirstRender = false,
}: {
renderer: WebGLRenderer
scene: Scene
camera: OrthographicCamera
controls: OrbitControls
isFirstRender?: boolean
}) => {
if (
!wrapperRef.current ||
!(isFirstRender || animationRequestRef.current)
)
return
animationRequestRef.current = requestAnimationFrame(() =>
animate({ renderer, scene, camera, controls })
)
// required if controls.enableDamping or controls.autoRotate are set to true
controls.update()
renderer.render(scene, camera)
},
[]
)
useEffect(() => {
if (!canvasRef.current) return
@ -132,7 +162,6 @@ export function ToastTextToCadSuccess({
const renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true })
renderer.setSize(CANVAS_SIZE, CANVAS_SIZE)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.setAnimationLoop(animate)
const scene = new Scene()
const ambientLight = new DirectionalLight(new Color('white'), 8.0)
@ -155,13 +184,6 @@ export function ToastTextToCadSuccess({
return
}
function animate() {
requestAnimationFrame(animate)
// required if controls.enableDamping or controls.autoRotate are set to true
controls.update()
renderer.render(scene, camera)
}
loader.parse(
buffer,
'',
@ -212,6 +234,8 @@ export function ToastTextToCadSuccess({
camera.updateProjectionMatrix()
controls.update()
// render the scene once...
renderer.render(scene, camera)
},
// called when loading has errors
function (error) {
@ -221,8 +245,26 @@ export function ToastTextToCadSuccess({
}
)
// ...and set a mouseover listener on the canvas to enable the orbit controls
canvasRef.current.addEventListener('mouseover', () => {
renderer.setAnimationLoop(() =>
animate({ renderer, scene, camera, controls, isFirstRender: true })
)
})
canvasRef.current.addEventListener('mouseout', () => {
renderer.setAnimationLoop(null)
if (animationRequestRef.current) {
cancelAnimationFrame(animationRequestRef.current)
animationRequestRef.current = undefined
}
})
return () => {
renderer.dispose()
if (animationRequestRef.current) {
cancelAnimationFrame(animationRequestRef.current)
animationRequestRef.current = undefined
}
}
}, [])
@ -251,6 +293,7 @@ export function ToastTextToCadSuccess({
iconStart={{
icon: 'close',
}}
data-negative-button={hasCopied ? 'close' : 'reject'}
name={hasCopied ? 'Close' : 'Reject'}
onClick={() => {
if (!hasCopied) {

View File

@ -14,6 +14,15 @@ const save_ = async (file: ModelingAppFile) => {
extensions.push(extension)
}
if (!(window as any).playwrightSkipFilePicker) {
// skip file picker, save to default location
await window.electron.writeFile(
file.name,
new Uint8Array(file.contents)
)
return
}
// Open a dialog to save the file.
const filePathMeta = await window.electron.save({
defaultPath: file.name,

View File

@ -112,7 +112,7 @@ const Home = () => {
).trim()
if (doesProjectNameNeedInterpolated(name)) {
const nextIndex = await getNextProjectIndex(name, projects)
const nextIndex = getNextProjectIndex(name, projects)
name = interpolateProjectNameWithIndex(name, nextIndex)
}

View File

@ -1428,6 +1428,7 @@ dependencies = [
"parse-display",
"pretty_assertions",
"pyo3",
"recursion",
"reqwest",
"ropey",
"schemars",
@ -2148,6 +2149,12 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "recursion"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f705426858ccd7bbfe19798239d6b6bfd9bf96bde0624a84b92694046e98871"
[[package]]
name = "redox_syscall"
version = "0.2.16"

View File

@ -31,6 +31,7 @@ lazy_static = "1.5.0"
mime_guess = "2.0.5"
parse-display = "0.9.1"
pyo3 = { version = "0.22.2", optional = true }
recursion = "0.5.2"
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }

View File

@ -1,2 +1,3 @@
pub mod modify;
mod recursion;
pub mod types;

View File

@ -0,0 +1,167 @@
use recursion::{Collapsible, MappableFrame, PartiallyApplied};
use super::types::{
BinaryExpression, BinaryOperator, BinaryPart, Digest, Expr, FnArgType, Identifier, KclNone, Literal, Parameter,
PipeSubstitution, TagDeclarator,
};
pub enum ExprFrame<A> {
Literal(Box<Literal>),
Identifier(Box<Identifier>),
TagDeclarator(Box<TagDeclarator>),
BinaryExpression(Box<BinaryExpressionFrame<A>>),
FunctionExpression(Box<FunctionExpressionFrame<A>>),
CallExpression(Box<CallExpressionFrame<A>>),
PipeExpression(Box<PipeExpressionFrame<A>>),
PipeSubstitution(Box<PipeSubstitution>),
ArrayExpression(Box<ArrayExpressionFrame<A>>),
ObjectExpression(Box<ObjectExpressionFrame<A>>),
MemberExpression(Box<MemberExpressionFrame<A>>),
UnaryExpression(Box<UnaryExpressionFrame<A>>),
None(KclNone),
}
impl MappableFrame for ExprFrame<PartiallyApplied> {
type Frame<X> = ExprFrame<X>;
fn map_frame<A, B>(input: Self::Frame<A>, mut f: impl FnMut(A) -> B) -> Self::Frame<B> {
match input {
ExprFrame::Literal(x) => ExprFrame::Literal(x),
ExprFrame::Identifier(x) => ExprFrame::Identifier(x),
ExprFrame::TagDeclarator(x) => ExprFrame::TagDeclarator(x),
ExprFrame::BinaryExpression(x) => ExprFrame::BinaryExpression(MappableFrame::map_frame(x, &mut f)),
ExprFrame::FunctionExpression(x) => ExprFrame::FunctionExpression(MappableFrame::map_frame(x, &mut f)),
ExprFrame::CallExpression(x) => ExprFrame::CallExpression(MappableFrame::map_frame(x, &mut f)),
ExprFrame::PipeExpression(x) => ExprFrame::PipeExpression(MappableFrame::map_frame(x, &mut f)),
ExprFrame::PipeSubstitution(x) => ExprFrame::PipeSubstitution(x),
ExprFrame::ArrayExpression(x) => ExprFrame::ArrayExpression(MappableFrame::map_frame(x, &mut f)),
ExprFrame::ObjectExpression(x) => ExprFrame::ObjectExpression(MappableFrame::map_frame(x, &mut f)),
ExprFrame::MemberExpression(x) => ExprFrame::MemberExpression(MappableFrame::map_frame(x, &mut f)),
ExprFrame::UnaryExpression(x) => ExprFrame::UnaryExpression(MappableFrame::map_frame(x, &mut f)),
ExprFrame::None(x) => ExprFrame::None(x),
}
}
}
impl<'a> Collapsible for &'a Expr {
type FrameToken = ExprFrame<PartiallyApplied>;
fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
match self {
Expr::Literal(x) => ExprFrame::Literal(x.clone()),
Expr::Identifier(x) => ExprFrame::Identifier(x.clone()),
Expr::TagDeclarator(x) => ExprFrame::TagDeclarator(x.clone()),
Expr::BinaryExpression(x) => ExprFrame::BinaryExpression(Box::new(x.into_frame())),
Expr::FunctionExpression(x) => ExprFrame::FunctionExpression(Box::new(x.into_frame())),
Expr::CallExpression(x) => ExprFrame::CallExpression(Box::new(x.into_frame())),
Expr::PipeExpression(x) => ExprFrame::PipeExpression(Box::new(x.into_frame())),
Expr::PipeSubstitution(x) => ExprFrame::PipeSubstitution(x.clone()),
Expr::ArrayExpression(x) => ExprFrame::ArrayExpression(Box::new(x.into_frame())),
Expr::ObjectExpression(x) => ExprFrame::ObjectExpression(Box::new(x.into_frame())),
Expr::MemberExpression(x) => ExprFrame::MemberExpression(Box::new(x.into_frame())),
Expr::UnaryExpression(x) => ExprFrame::UnaryExpression(Box::new(x.into_frame())),
Expr::None(x) => ExprFrame::None(x.clone()),
}
}
}
pub struct BinaryExpressionFrame<A> {
pub start: usize,
pub end: usize,
pub operator: BinaryOperator,
pub left: BinaryPartFrame<A>,
pub right: BinaryPartFrame<A>,
pub digest: Option<Digest>,
}
impl MappableFrame for BinaryExpressionFrame<PartiallyApplied> {
type Frame<X> = BinaryExpressionFrame<X>;
fn map_frame<A, B>(input: Self::Frame<A>, mut f: impl FnMut(A) -> B) -> Self::Frame<B> {
BinaryExpressionFrame::<B> {
start: input.start,
end: input.end,
operator: input.operator,
left: <BinaryPartFrame<PartiallyApplied> as MappableFrame>::map_frame(input.left, &mut f),
right: <BinaryPartFrame<PartiallyApplied> as MappableFrame>::map_frame(input.right, &mut f),
digest: input.digest,
}
}
}
impl<'a> Collapsible for &'a BinaryExpression {
type FrameToken = BinaryExpressionFrame<PartiallyApplied>;
fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
BinaryExpressionFrame::<BinaryExpression> {
start: self.start,
end: self.end,
operator: self.operator.clone(),
left: self.left.into_frame(),
right: self.right.into_frame(),
digest: self.digest,
}
}
}
pub enum BinaryPartFrame<A> {
Literal(Box<Literal>),
Identifier(Box<Identifier>),
BinaryExpression(Box<BinaryExpressionFrame<A>>),
CallExpression(Box<CallExpressionFrame<A>>),
UnaryExpression(Box<UnaryExpressionFrame<A>>),
MemberExpression(Box<MemberExpressionFrame<A>>),
}
impl MappableFrame for BinaryPartFrame<PartiallyApplied> {
type Frame<X> = BinaryPartFrame<X>;
fn map_frame<A, B>(input: Self::Frame<A>, mut f: impl FnMut(A) -> B) -> Self::Frame<B> {
match input {
BinaryPartFrame::Literal(x) => BinaryPartFrame::Literal(x),
BinaryPartFrame::Identifier(x) => BinaryPartFrame::Identifier(x),
BinaryPartFrame::BinaryExpression(x) => BinaryPartFrame::BinaryExpression(MappableFrame::map_frame(x, f)),
BinaryPartFrame::CallExpression(x) => BinaryPartFrame::CallExpression(MappableFrame::map_frame(x, f)),
BinaryPartFrame::UnaryExpression(x) => BinaryPartFrame::UnaryExpression(MappableFrame::map_frame(x, f)),
BinaryPartFrame::MemberExpression(x) => BinaryPartFrame::MemberExpression(MappableFrame::map_frame(x, f)),
}
}
}
impl<'a> Collapsible for &'a BinaryPart {
type FrameToken = BinaryPartFrame<PartiallyApplied>;
fn into_frame(self) -> <Self::FrameToken as MappableFrame>::Frame<Self> {
match self {
BinaryPart::Literal(x) => BinaryPartFrame::Literal(x.clone()),
BinaryPart::Identifier(x) => BinaryPartFrame::Identifier(x.clone()),
BinaryPart::BinaryExpression(x) => BinaryPartFrame::BinaryExpression(x.into_frame()),
BinaryPart::CallExpression(x) => BinaryPartFrame::CallExpression(x.into_frame()),
BinaryPart::UnaryExpression(x) => BinaryPartFrame::UnaryExpression(x.into_frame()),
BinaryPart::MemberExpression(x) => BinaryPartFrame::MemberExpression(x.into_frame()),
}
}
}
pub struct FunctionExpressionFrame<A> {
pub start: usize,
pub end: usize,
pub params: Vec<Parameter>,
pub body: ProgramFrame<A>,
pub return_type: Option<FnArgType>,
pub digest: Option<Digest>,
}
pub struct CallExpressionFrame<A> {
pub start: usize,
pub end: usize,
pub callee: Identifier,
pub arguments: Vec<ExprFrame<A>>,
pub optional: bool,
pub digest: Option<Digest>,
}
// More...