Compare commits

...

45 Commits

Author SHA1 Message Date
682590deea KCL parser: allow comments in multi-line arrays (#3539)
KCL parser: allow noncode (e.g. comments) in arrays

Part of #1528
2024-08-21 09:54:15 -05:00
925f5cc2c2 windows doesn't like cp, in build wasm script (#3599) 2024-08-21 15:55:59 +10:00
a167c174f9 add another skip to make windows tron happy (#3597)
add another skip to make windows happy
2024-08-21 15:23:01 +10:00
7f297c13fd Test for reconnect and theme (#3264)
add test
2024-08-21 15:02:54 +10:00
a7e3d83297 Test for 3 export paths (#3596)
test for 3 export paths #3582
2024-08-21 15:02:39 +10:00
f74c12aa99 Bump @types/react from 18.3.3 to 18.3.4 (#3595)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.3.3 to 18.3.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-20 21:15:00 -07:00
5df9965795 Bump @types/node from 22.4.1 to 22.4.2 (#3594)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.4.1 to 22.4.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-20 21:14:45 -07:00
50d80eb0b6 Bump electron from 31.4.0 to 32.0.1 (#3593)
Bumps [electron](https://github.com/electron/electron) from 31.4.0 to 32.0.1.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v31.4.0...v32.0.1)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-20 21:14:32 -07:00
96d24065d6 Bump google-github-actions/auth from 2.1.3 to 2.1.5 (#3589)
Bumps [google-github-actions/auth](https://github.com/google-github-actions/auth) from 2.1.3 to 2.1.5.
- [Release notes](https://github.com/google-github-actions/auth/releases)
- [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/auth/compare/v2.1.3...v2.1.5)

---
updated-dependencies:
- dependency-name: google-github-actions/auth
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-20 21:12:46 -07:00
61dc94b1ee Show the SketchGroup can be found even if it's deeper in an object (#3462)
* Show the SketchGroup can be found even if it's deeper in an object

* trigger rust

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
2024-08-20 21:12:38 -07:00
f14c27e1c4 Add the unexpected token to the error message (#3430)
* Add the unexpected token to the error message

This is helpful during development where there's nothing to point at
the character index.

* Update more tests
2024-08-20 20:49:19 -07:00
c09775f5eb Fix existing: file renaming (and more things that spin out of settings file path parsing) (#3584)
* Fix the behavior so that we navigate to the new file path

* This change is done in other PRs but is also necessary here

* Add an Electron Playwright test for renaming a file

* Add tests for renaming dir, one is failing

* Don't need that console.warn

* Add DeepPartial utility type

* Fix settings parsing so that project path parsing is fixed

* Move URL check after DOM checks

* Revert this fallback behavior from https://github.com/KittyCAD/modeling-app/pull/3564 as we don't need it now that config parsing is fixed

* Make new bad prompt each run

* Fix onboarding asset path in web

* Remove double parsing of settings config

* Remove unused imports

* More unused imports

* Fix broken rename test

* Update src/lib/desktop.ts

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Add test for renaming file we do not have open

* fmt

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-08-20 22:16:44 -04:00
d14b8f5443 Add electron test for persisting open panes (#3535)
* Add electron test for persisting open panes

* Debugging persistence across test runs

* Trigger addInitScript for electron

* Remove init of PERSIST_MODELING_CONTEXT key

* Remove unused code

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

---------

Co-authored-by: 49lf <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-08-20 19:11:21 -07:00
4a14ca38ab Fix to convert electron platform name darwin to macos (#3573)
* Fix to convert electron platform name darwin to macos

* Remove unneeded async

* Fix to handle other possible platform strings

* Add electron test for user sidebar menu text

* Fix formatting
2024-08-20 19:08:02 -07:00
3543c5f0e7 Update tests after ben merge of fillet working w shell (#3586)
* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* bump all the things

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-20 18:57:23 -07:00
a0dc5f4a89 Update machine-api spec (#3585)
* YOYO NEW API SPEC!

* New machine-api types

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-20 17:13:18 -07:00
9d148938a2 Fix app version in prod (#3579)
* fix app version, make more dry

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

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

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

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

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

* empty

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

* empty

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

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

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

* empty

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-20 15:15:25 -07:00
9c6cca2944 Revert "reflow machines from k/v" (#3575)
This reverts commit f77b312ecb.
2024-08-20 14:07:32 -07:00
5c472c63d2 fix for when response status is not okay (#3574)
* fix for when response status is not okay

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-20 13:40:21 -07:00
f77b312ecb reflow machines from k/v 2024-08-20 16:28:41 -04:00
8a66d0df76 Fix a couple issues with settings reset on web & electron (#3564)
* Fix a couple issues with settings reset on web & electron

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-08-20 11:06:39 -07:00
b3dc3ff78c Bump google-github-actions/upload-cloud-storage from 2.1.0 to 2.1.3 (#3556)
Bumps [google-github-actions/upload-cloud-storage](https://github.com/google-github-actions/upload-cloud-storage) from 2.1.0 to 2.1.3.
- [Release notes](https://github.com/google-github-actions/upload-cloud-storage/releases)
- [Changelog](https://github.com/google-github-actions/upload-cloud-storage/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/upload-cloud-storage/compare/v2.1.0...v2.1.3)

---
updated-dependencies:
- dependency-name: google-github-actions/upload-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-20 10:16:18 -07:00
d02df08471 Bump actions/github-script from 4 to 7 (#3555)
Bumps [actions/github-script](https://github.com/actions/github-script) from 4 to 7.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-20 10:16:02 -07:00
aac758b396 Run yarn format (#3566) 2024-08-20 10:22:37 -05:00
0ef6eac239 Fix existing: when engine fails export we handle the failure and alert the user (#3561)
Fix existing: when engine fails export we handle the failure and alert the user #3560
2024-08-20 20:29:15 +10:00
c674feb782 Update the Web Banner (#3563)
Update the Web Banner #3503
2024-08-20 20:28:39 +10:00
fba3d7c5c1 skip failing windows tests (#3558) 2024-08-20 06:37:16 +00:00
8b8fb696d0 Fix existing: Can edit a sketch that has been extruded in the same pipe (#3552)
Fix existing: Can edit a sketch that has been extruded in the same pipe #3551
2024-08-20 14:13:56 +10:00
d05f3c00b9 test keyboard shortcuts from help menu (#3553)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-19 20:17:23 -07:00
2541e0c0ea Electron test (regression): select all in code editor does not actually select all, just what is visiable (#3540)
* select all in code editor does not actually select all, just what is visible #3175

* whops

* fix test for linux
2024-08-20 11:23:32 +10:00
5e5a204244 move the file (#3544)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-19 17:55:26 -07:00
032c2fdd24 Add GitHub action to label issues opened by ZooSpiritWolf (#3543)
* Add GitHub action to label issues opened by ZooSpiritWolf

Fixes #3541

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/KittyCAD/modeling-app/issues/3541?shareId=XXXX-XXXX-XXXX-XXXX).

* Update label-issues.yml
2024-08-19 17:54:24 -07:00
27883e7800 Electron machine api tests (#3534)
* start

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Look at this (photo)Graph *in the voice of Nickelback*

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* hide on webapp

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix machine-api

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-19 15:57:31 -07:00
1ccb810e23 Revert "electron test code with error loading" (#3533)
Revert "electron test code with error loading (#3531)"

This reverts commit c7f533b38e.
2024-08-19 14:23:01 -07:00
1c83f148d9 export on project/file pane load playwright test (#3489)
* export on project/file pane load

Signed-off-by: Jess Frazelle <github@jessfraz.com>

add desktop exxport

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Look at this (photo)Graph *in the voice of Nickelback*

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix lint

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixeds

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixups

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-19 14:21:34 -07:00
c7f533b38e electron test code with error loading (#3531)
* electron test code with error loading

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Look at this (photo)Graph *in the voice of Nickelback*

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-19 13:54:11 -07:00
2b711d216f Run eslint in CI (#3487)
* Run eslint in CI

* Add linting of e2e

* Fix formatting

* Fix new warnings in e2e

* Fix more new warnings
2024-08-19 12:36:18 -07:00
c67511f67c Electron test: (regression) you can scroll the file pane when ETOOMANYFILES to view in one view (#3520)
* can scroll files pane

* clean up
2024-08-20 05:34:26 +10:00
d9423219d1 Bump @csstools/postcss-oklab-function from 3.0.19 to 4.0.2 (#3517)
Bumps [@csstools/postcss-oklab-function](https://github.com/csstools/postcss-plugins/tree/HEAD/plugins/postcss-oklab-function) from 3.0.19 to 4.0.2.
- [Changelog](https://github.com/csstools/postcss-plugins/blob/main/plugins/postcss-oklab-function/CHANGELOG.md)
- [Commits](https://github.com/csstools/postcss-plugins/commits/HEAD/plugins/postcss-oklab-function)

---
updated-dependencies:
- dependency-name: "@csstools/postcss-oklab-function"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 12:29:10 -07:00
3f270d8bcf Bump react-router-dom from 6.26.0 to 6.26.1 (#3516)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.26.0 to 6.26.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.26.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 12:28:59 -07:00
4c7b72329d Bump @playwright/test from 1.45.3 to 1.46.1 (#3514)
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.45.3 to 1.46.1.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.45.3...v1.46.1)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 12:27:12 -07:00
4c060f3d2f Bump electron from 31.3.1 to 31.4.0 (#3515)
Bumps [electron](https://github.com/electron/electron) from 31.3.1 to 31.4.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v31.3.1...v31.4.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 12:26:53 -07:00
f3afbe8a7b Bump @types/node from 18.19.42 to 22.4.1 (#3513)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.19.42 to 22.4.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 12:26:36 -07:00
dad7a84798 Bump tokio from 1.39.2 to 1.39.3 in /src/wasm-lib (#3511)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.39.2 to 1.39.3.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.39.2...tokio-1.39.3)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 12:26:03 -07:00
1a560fdc6a Bump syn from 2.0.74 to 2.0.75 in /src/wasm-lib (#3510)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.74 to 2.0.75.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.74...2.0.75)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 12:25:53 -07:00
120 changed files with 2599 additions and 597 deletions

View File

@ -25,7 +25,9 @@
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
"rules": {
"@typescript-eslint/no-floating-promises": "warn",
"testing-library/prefer-screen-queries": "off"
"suggest-no-throw/suggest-no-throw": "off",
"testing-library/prefer-screen-queries": "off",
"jest/valid-expect": "off"
}
},
{

View File

@ -345,7 +345,7 @@ jobs:
cat last_download.json
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.3'
uses: 'google-github-actions/auth@v2.1.5'
with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
@ -355,7 +355,7 @@ jobs:
project_id: kittycadapi
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
uses: google-github-actions/upload-cloud-storage@v2.1.3
with:
path: artifact
glob: '*/Zoo*'
@ -363,13 +363,13 @@ jobs:
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
uses: google-github-actions/upload-cloud-storage@v2.1.3
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
uses: google-github-actions/upload-cloud-storage@v2.1.3
with:
path: last_download.json
destination: ${{ env.BUCKET_DIR }}

View File

@ -44,6 +44,8 @@ jobs:
- run: yarn build:wasm
- run: yarn xstate:typegen
- run: yarn tsc
- name: Lint
run: yarn eslint --max-warnings 0 src e2e
check-typos:

View File

@ -7,6 +7,7 @@ on:
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'src/wasm-lib/**.kcl'
- .github/workflows/cargo-test.yml
pull_request:
@ -15,6 +16,7 @@ on:
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'src/wasm-lib/**.kcl'
- .github/workflows/cargo-test.yml
workflow_dispatch:
permissions: read-all

31
.github/workflows/label-issues.yml vendored Normal file
View File

@ -0,0 +1,31 @@
name: Label Issues
on:
issues:
types: [opened]
permissions:
issues: write
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Check if issue opener is ZooSpiritWolf
id: check_opener
uses: actions/github-script@v7
with:
script: |
const issueOpener = context.payload.issue.user.login;
return issueOpener === 'ZooSpiritWolf';
- name: Add labels
if: steps.check_opener.outputs.result == 'true'
uses: actions/github-script@v7
with:
script: |
github.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
labels: ['bug', 'regression', 'high-priority']
});

View File

@ -346,7 +346,7 @@ jobs:
run: yarn build:wasm
- name: build electron
shell: bash
run: yarn electron:package
run: yarn tron:package
- uses: actions/download-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true

View File

@ -101,7 +101,7 @@ This will start the application and hot-reload on changed.
Devtools can be opened with the usual Cmd/Ctrl-Shift-I.
To build, run `yarn electron:package`.
To build, run `yarn tron:package`.
## Checking out commits / Bisecting

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,9 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
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)
@ -83,7 +84,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.getByText('Unexpected token: |').first()).toBeVisible()
// Close the code pane
await codePaneButton.click()
@ -106,7 +107,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.getByText('Unexpected token: |').first()).toBeVisible()
})
test('When error is not in view you can click the badge to scroll to it', async ({
@ -217,3 +218,93 @@ test.describe('Code pane and errors', () => {
).toBeVisible()
})
})
test(
'Opening multiple panes persists when switching projects',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
// Setup multiple projects.
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await Promise.all([
fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }),
fsp.mkdir(`${dir}/bracket`, { recursive: true }),
])
await Promise.all([
fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/router-template-slate/main.kcl`
),
fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/bracket/main.kcl`
),
])
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await test.step('Opening the bracket project should load', async () => {
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,
})
})
// If they're open by default, we're not actually testing anything.
await test.step('Pre-condition: panes are not already visible', async () => {
await expect(page.locator('#variables-pane')).not.toBeVisible()
await expect(page.locator('#logs-pane')).not.toBeVisible()
})
await test.step('Open multiple panes', async () => {
await u.openKclCodePanel()
await u.openVariablesPane()
await u.openLogsPane()
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
await page.getByTestId('app-logo').click()
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('Opening the router-template project should load', async () => {
await expect(page.getByText('router-template-slate')).toBeVisible()
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 test.step('All panes opened before should be visible', async () => {
await expect(page.locator('#code-pane')).toBeVisible()
await expect(page.locator('#variables-pane')).toBeVisible()
await expect(page.locator('#logs-pane')).toBeVisible()
})
await electronApp.close()
}
)

View File

@ -9,6 +9,7 @@ test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Copilot ghost text', () => {
// eslint-disable-next-line jest/valid-title
test.skip(true, 'Needs to get covered again')
test('completes code in empty file', async ({ page }) => {

View File

@ -0,0 +1,188 @@
import { test, expect } from '@playwright/test'
import { getUtils, setupElectron, tearDown } from './test-utils'
import fsp from 'fs/promises'
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test(
'export works on the first try',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await Promise.all([fsp.mkdir(`${dir}/bracket`, { recursive: true })])
await Promise.all([
fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/bracket/other.kcl`
),
fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/bracket/main.kcl`
),
])
},
})
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await test.step('on open of project', async () => {
await expect(page.getByText(`bracket`)).toBeVisible()
// 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()
// export the model
const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible()
const gltfOption = page.getByText('glTF')
const submitButton = page.getByText('Confirm Export')
const exportingToastMessage = page.getByText(`Exporting...`)
const errorToastMessage = page.getByText(`Error while exporting`)
const engineErrorToastMessage = page.getByText(`Nothing to export`)
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
// Click the export button
await exportButton.click()
await expect(gltfOption).toBeVisible()
await expect(page.getByText('STL')).toBeVisible()
await page.keyboard.press('Enter')
// Click the checkbox
await expect(submitButton).toBeVisible()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
// Find the toast.
// Look out for the toast message
await expect(exportingToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible()
// Expect it to succeed.
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
const successToastMessage = page.getByText(`Exported successfully`)
await expect(successToastMessage).toBeVisible()
await expect(exportingToastMessage).not.toBeVisible()
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 test.step('on open of file in file pane', async () => {
const u = await getUtils(page)
await u.openFilePanel()
const otherKclButton = page.getByRole('button', { name: 'other.kcl' })
// Click the file
await otherKclButton.click()
// Close the file pane
await u.closeFilePanel()
// wait for it to finish executing (todo: make this more robust)
await page.waitForTimeout(1000)
// expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// export the model
const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible()
const gltfOption = page.getByText('glTF')
const submitButton = page.getByText('Confirm Export')
const exportingToastMessage = page.getByText(`Exporting...`)
const errorToastMessage = page.getByText(`Error while exporting`)
const engineErrorToastMessage = page.getByText(`Nothing to export`)
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
// Click the export button
await exportButton.click()
await expect(gltfOption).toBeVisible()
await expect(page.getByText('STL')).toBeVisible()
await page.keyboard.press('Enter')
// Click the checkbox
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
// Find the toast.
// Look out for the toast message
await expect(exportingToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible()
// Expect it to succeed.
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
const successToastMessage = page.getByText(`Exported successfully`)
await expect(successToastMessage).toBeVisible()
await expect(exportingToastMessage).not.toBeVisible()
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(105022)
// clean up output.gltf
await fsp.rm('output.gltf')
})
await electronApp.close()
})
await electronApp.close()
}
)

View File

@ -354,7 +354,7 @@ test.describe('Editor tests', () => {
// error text on hover
await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token').first()).toBeVisible()
await expect(page.getByText('Unexpected token: $').first()).toBeVisible()
// select the line that's causing the error and delete it
await page.getByText('$ error').click()
@ -714,17 +714,15 @@ test.describe('Editor tests', () => {
|> close(%)`)
})
// failing for the same reason as "Can edit a sketch that has been extruded in the same pipe"
// please fix together
test.fixme('Can undo a sketch modification with ctrl+z', async ({ page }) => {
test('Can undo a sketch modification with ctrl+z', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> startProfileAt([4.61, -10.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> tangentialArcTo([24.95, -0.38], %)
|> close(%)
|> extrude(5, %)`
)
@ -759,11 +757,11 @@ test.describe('Editor tests', () => {
})
await page.waitForTimeout(100)
const startPX = [665, 458]
const startPX = [665, 397]
const dragPX = 40
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
await page.getByText('startProfileAt([4.61, -10.01], %)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
@ -801,7 +799,7 @@ test.describe('Editor tests', () => {
// drag tangentialArcTo handle
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: tangentEnd.x, y: tangentEnd.y - 5 },
sourcePosition: { x: tangentEnd.x + 10, y: tangentEnd.y - 5 },
targetPosition: {
x: tangentEnd.x + dragPX,
y: tangentEnd.y + dragPX,
@ -813,12 +811,12 @@ test.describe('Editor tests', () => {
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -16.82], %)
|> line([15.4, -2.74], %)
|> tangentialArcTo([24.95, -5.38], %)
|> line([2.65, -2.69], %)
|> close(%)
|> extrude(5, %)`)
|> startProfileAt([7.12, -12.68], %)
|> line([15.39, -2.78], %)
|> tangentialArcTo([27.6, -3.05], %)
|> close(%)
|> extrude(5, %)
`)
// Hit undo
await page.keyboard.down('Control')
@ -827,11 +825,11 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -16.82], %)
|> line([15.4, -2.74], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)
|> extrude(5, %)`)
|> startProfileAt([7.12, -12.68], %)
|> line([15.39, -2.78], %)
|> tangentialArcTo([24.95, -0.38], %)
|> close(%)
|> extrude(5, %)`)
// Hit undo again.
await page.keyboard.down('Control')
@ -840,11 +838,12 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -16.82], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> close(%)
|> extrude(5, %)`)
|> startProfileAt([7.12, -12.68], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -0.38], %)
|> close(%)
|> extrude(5, %)
`)
// Hit undo again.
await page.keyboard.down('Control')
@ -854,9 +853,9 @@ test.describe('Editor tests', () => {
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
|> startProfileAt([4.61, -10.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> tangentialArcTo([24.95, -0.38], %)
|> close(%)
|> extrude(5, %)`)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -0,0 +1,102 @@
import { test, expect } from '@playwright/test'
import { setupElectron, tearDown } from './test-utils'
import fsp from 'fs/promises'
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test(
'When machine-api server not found butt is disabled and shows the reason',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
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 })
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,
})
const notFoundText = 'Machine API server was not discovered'
await expect(page.getByText(notFoundText).first()).not.toBeVisible()
// Find the make button
const makeButton = page.getByRole('button', { name: 'Make' })
// Make sure the button is visible but disabled
await expect(makeButton).toBeVisible()
await expect(makeButton).toBeDisabled()
// When you hover over the button, the tooltip should show
// that the machine-api server is not found
await makeButton.hover()
await expect(page.getByText(notFoundText).first()).toBeVisible()
await electronApp.close()
}
)
test(
'When machine-api server not found home screen & project status shows the reason',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
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 notFoundText = 'Machine API server was not discovered'
await expect(page.getByText(notFoundText)).not.toBeVisible()
const networkMachineToggle = page.getByTestId('network-machine-toggle')
await networkMachineToggle.hover()
await expect(page.getByText(notFoundText)).toBeVisible()
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.getByText(notFoundText).nth(1)).not.toBeVisible()
await networkMachineToggle.hover()
await expect(page.getByText(notFoundText).nth(1)).toBeVisible()
await electronApp.close()
}
)

View File

@ -347,6 +347,10 @@ test(
'Restarting onboarding on desktop takes one attempt',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {

View File

@ -2,6 +2,7 @@ import { test, expect } from '@playwright/test'
import {
doExport,
getUtils,
isOutOfViewInScrollContainer,
Paths,
setupElectron,
tearDown,
@ -9,15 +10,45 @@ 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)
})
test(
'Can export from electron app',
'click help/keybindings from home page',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// click ? button
await page.getByTestId('help-button').click()
await expect(page.getByTestId('keybindings-button')).toBeVisible()
// Click keyboard shortcuts button.
await page.getByTestId('keybindings-button').click()
// Make sure the keyboard shortcuts modal is visible.
await expect(page.getByText('Enter Sketch Mode')).toBeVisible()
await electronApp.close()
}
)
test(
'click help/keybindings from project page',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
@ -30,82 +61,181 @@ test(
})
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
page.on('console', console.log)
// 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,
})
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')
})
// click ? button
await page.getByTestId('help-button').click()
await expect(page.getByTestId('keybindings-button')).toBeVisible()
// Click keyboard shortcuts button.
await page.getByTestId('keybindings-button').click()
// Make sure the keyboard shortcuts modal is visible.
await expect(page.getByText('Enter Sketch Mode')).toBeVisible()
await electronApp.close()
}
)
test(
'when code with error first loads you get errors in console',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/broken-code`, { recursive: true })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/broken-code-test.kcl',
`${dir}/broken-code/main.kcl`
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
await expect(page.getByText('broken-code')).toBeVisible()
await page.getByText('broken-code').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
const crypticErrorText = `Expected a tag declarator`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
await electronApp.close()
}
)
test.describe('Can export from electron app', () => {
const exportMethods = ['sidebarButton', 'commandBar'] as const
for (const method of exportMethods) {
test(
`Can export using ${method}`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
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 using ${method}`, async () => {
exportLocations.push(
await doExport(
{
type: 'gltf',
storage: 'embedded',
presentation: 'pretty',
},
page,
method
)
)
})
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' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
@ -394,6 +524,10 @@ test(
'Deleting projects, can delete individual project, can still create projects after deleting all',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
})
@ -488,6 +622,10 @@ test(
'Can sort projects on home page',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
})
@ -610,6 +748,10 @@ test(
'When the project folder is empty, user can create new project and open it.',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({ testInfo })
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
@ -694,6 +836,10 @@ test(
'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
@ -907,6 +1053,10 @@ test(
'Search projects on desktop home',
{ tag: '@electron' },
async ({ browserName: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const projectData = [
['basic bracket', 'focusrite_scarlett_mounting_braket.kcl'],
['basic-cube', 'basic_fillet_cube_end.kcl'],
@ -964,10 +1114,198 @@ test(
}
)
test(
'file pane is scrollable when there are many files',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/testProject`, { recursive: true })
const fileNames = [
'angled_line.kcl',
'basic_fillet_cube_close_opposite.kcl',
'basic_fillet_cube_end.kcl',
'basic_fillet_cube_next_adjacent.kcl',
'basic_fillet_cube_previous_adjacent.kcl',
'basic_fillet_cube_start.kcl',
'big_number_angle_to_match_length_x.kcl',
'big_number_angle_to_match_length_y.kcl',
'close_arc.kcl',
'computed_var.kcl',
'cube-embedded.gltf',
'cube.bin',
'cube.glb',
'cube.gltf',
'cube.kcl',
'cube.mtl',
'cube.obj',
'cylinder.kcl',
'dimensions_match.kcl',
'extrude-custom-plane.kcl',
'extrude-inside-fn-with-tags.kcl',
'fillet-and-shell.kcl',
'fillet_duplicate_tags.kcl',
'focusrite_scarlett_mounting_braket.kcl',
'function_sketch.kcl',
'function_sketch_with_position.kcl',
'global-tags.kcl',
'helix_ccw.kcl',
'helix_defaults.kcl',
'helix_defaults_negative_extrude.kcl',
'helix_with_length.kcl',
'i_shape.kcl',
'kittycad_svg.kcl',
'lego.kcl',
'math.kcl',
'member_expression_sketch_group.kcl',
'mike_stress_test.kcl',
'negative_args.kcl',
'order-sketch-extrude-in-order.kcl',
'order-sketch-extrude-out-of-order.kcl',
'parametric.kcl',
'parametric_with_tan_arc.kcl',
'pattern_vase.kcl',
'pentagon_fillet_sugar.kcl',
'pipe_as_arg.kcl',
'pipes_on_pipes.kcl',
'riddle.kcl',
'riddle_small.kcl',
'router-template-slate.kcl',
'scoped-tags.kcl',
'server-rack-heavy.kcl',
'server-rack-lite.kcl',
'sketch_on_face.kcl',
'sketch_on_face_circle_tagged.kcl',
'sketch_on_face_end.kcl',
'sketch_on_face_end_negative_extrude.kcl',
'sketch_on_face_start.kcl',
'tan_arc_x_line.kcl',
'tangential_arc.kcl',
]
for (const fileName of fileNames) {
await fsp.copyFile(
`src/wasm-lib/tests/executor/inputs/${fileName}`,
`${dir}/testProject/${fileName}`
)
}
},
})
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await test.step('setup, open file pane', async () => {
await page.getByText('testProject').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await page.getByTestId('files-pane-button').click()
})
await test.step('check the last file is out of view initially, and can be scrolled to', async () => {
const element = page.getByText('tangential_arc.kcl')
const container = page.getByTestId('file-pane-scroll-container')
await expect(await isOutOfViewInScrollContainer(element, container)).toBe(
true
)
await element.scrollIntoViewIfNeeded()
await expect(await isOutOfViewInScrollContainer(element, container)).toBe(
false
)
})
await electronApp.close()
}
)
test(
'select all in code editor does not actually select all, just what is visible (regression)',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
// src/wasm-lib/tests/executor/inputs/mike_stress_test.kcl
const name = 'mike_stress_test'
await fsp.mkdir(`${dir}/${name}`, { recursive: true })
await fsp.copyFile(
`src/wasm-lib/tests/executor/inputs/${name}.kcl`,
`${dir}/${name}/main.kcl`
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await page.getByText('mike_stress_test').click()
const modifier =
process.platform === 'win32' || process.platform === 'linux'
? 'Control'
: 'Meta'
await test.step('select all in code editor, check its length', async () => {
await u.codeLocator.click()
// expect u.codeLocator to have some text
await expect(u.codeLocator).toContainText('line(')
await page.keyboard.down(modifier)
await page.keyboard.press('KeyA')
await page.keyboard.up(modifier)
// check the length of the selected text
const selectedText = await page.evaluate(() => {
const selection = window.getSelection()
return selection ? selection.toString() : ''
})
// even though if the user copied the text into their clipboard they would get the full text
// it seems that the selection is limited to what is visible
// we just want to check we did select something, and later we've verify it's empty
expect(selectedText.length).toBeGreaterThan(10)
})
await test.step('delete all the text, select again and verify there are no characters left', async () => {
await page.keyboard.press('Backspace')
await page.keyboard.down(modifier)
await page.keyboard.press('KeyA')
await page.keyboard.up(modifier)
// check the length of the selected text
const selectedText = await page.evaluate(() => {
const selection = window.getSelection()
return selection ? selection.toString() : ''
})
expect(selectedText.length).toBe(0)
await expect(u.codeLocator).toHaveText('')
})
await electronApp.close()
}
)
test(
'Settings persist across restarts',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
await test.step('We can change a user setting like theme', async () => {
const { electronApp, page } = await setupElectron({
testInfo,
@ -1006,3 +1344,369 @@ test(
})
}
)
test.describe('Renaming in the file tree', () => {
test(
'A file you have open',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
const exampleDir = join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs'
)
await fsp.copyFile(
join(exampleDir, 'basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
join(exampleDir, '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 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 newFileName = 'newFileName'
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()
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()
})
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) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
const exampleDir = join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs'
)
await fsp.copyFile(
join(exampleDir, 'basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
join(exampleDir, '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 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()
})
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()
})
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) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
recursive: true,
})
const exampleDir = join(
'src',
'wasm-lib',
'tests',
'executor',
'inputs'
)
await fsp.copyFile(
join(exampleDir, 'basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
join(exampleDir, '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 renameInput = page.getByPlaceholder('folderToRename')
const newFolderName = 'newFolderName'
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()
})
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()
})
await electronApp.close()
}
)
test(
`A folder you are inside`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const exampleDir = join('src', 'wasm-lib', 'tests', 'executor', 'inputs')
const { electronApp, page } = await setupElectron({
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(
join(exampleDir, 'basic_fillet_cube_end.kcl'),
join(dir, 'Test Project', 'main.kcl')
)
await fsp.copyFile(
join(exampleDir, '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 renameInput = page.getByPlaceholder('folderToRename')
const newFolderName = 'newFolderName'
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')
})
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 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')
})
await electronApp.close()
}
)
})

View File

@ -194,7 +194,7 @@ const sketch001 = startSketchAt([-0, -0])
// error text on hover
await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token').first()).toBeVisible()
await expect(page.getByText('Unexpected token: |').first()).toBeVisible()
// Okay execution finished, let's start editing text below the error.
await u.codeLocator.click()
@ -236,9 +236,13 @@ const sketch001 = startSketchAt([-0, -0])
page,
}) => {
const u = await getUtils(page)
await page.addInitScript(async (code) => {
localStorage.setItem('persistCode', code)
}, TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR)
await page.addInitScript(
async ({ code }) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
},
{ code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR }
)
await page.setViewportSize({ width: 1000, height: 500 })
@ -325,7 +329,7 @@ const sketch001 = startSketchAt([-0, -0])
await expect(exportingToastMessage).toBeVisible()
// Expect it to succeed.
await expect(exportingToastMessage).not.toBeVisible()
await expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 })
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
@ -337,6 +341,7 @@ const sketch001 = startSketchAt([-0, -0])
}) => {
// This is being weird on ubuntu and windows.
test.skip(
// eslint-disable-next-line jest/valid-title
process.platform === 'linux' || process.platform === 'win32',
'This test is being weird on ubuntu'
)
@ -420,6 +425,10 @@ const sketch001 = startSketchAt([-0, -0])
`Network health indicator only appears in modeling view`,
{ tag: '@electron' },
async ({ browserName: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
@ -472,7 +481,7 @@ async function clickExportButton(page: Page) {
// Click the export button
await exportButton.click()
// Click the stl.
// Click the gltf.
const gltfOption = page.getByRole('option', { name: 'glTF' })
await expect(gltfOption).toBeVisible()

View File

@ -344,111 +344,108 @@ test.describe('Sketch tests', () => {
})
})
// failing for the same reason as "Can undo a sketch modification with ctrl+z"
// please fix together
test.fixme(
'Can edit a sketch that has been extruded in the same pipe',
async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -14.01], %)
test('Can edit a sketch that has been extruded in the same pipe', async ({
page,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61, -10.01], %)
|> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -5.38], %)
|> tangentialArcTo([24.95, -0.38], %)
|> close(%)
|> extrude(5, %)`
)
})
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await u.waitForAuthSkipAppStart()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
const startPX = [665, 458]
const startPX = [665, 397]
const dragPX = 40
const dragPX = 40
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText()
await page.getByText('startProfileAt([4.61, -10.01], %)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText()
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
// drag startProfieAt handle
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0], y: startPX[1] },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag startProfieAt handle
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0], y: startPX[1] },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag line handle
await page.waitForTimeout(100)
// drag line handle
await page.waitForTimeout(100)
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.waitForTimeout(100)
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX },
})
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.waitForTimeout(100)
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX },
})
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag tangentialArcTo handle
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: tangentEnd.x, y: tangentEnd.y - 5 },
targetPosition: {
x: tangentEnd.x + dragPX,
y: tangentEnd.y + dragPX,
},
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// drag tangentialArcTo handle
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: tangentEnd.x + 10, y: tangentEnd.y - 5 },
targetPosition: {
x: tangentEnd.x + dragPX,
y: tangentEnd.y + dragPX,
},
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -16.82], %)
|> line([15.4, -2.74], %)
|> tangentialArcTo([24.95, -5.38], %)
|> line([2.65, -2.69], %)
|> close(%)
|> extrude(5, %)`)
}
)
// expect the code to have changed
await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -12.68], %)
|> line([15.39, -2.78], %)
|> tangentialArcTo([27.6, -3.05], %)
|> close(%)
|> extrude(5, %)
`)
})
test('Can edit a sketch that has been revolved in the same pipe', async ({
page,
@ -602,7 +599,7 @@ test.describe('Sketch tests', () => {
await expect(u.codeLocator).toHaveText(codeStr)
// exit the sketch, reset relative clicker
click00r(undefined, undefined)
await click00r(undefined, undefined)
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')

View File

@ -53,6 +53,7 @@ test(
async ({ page, context }) => {
// skip on macos and windows.
test.skip(
// eslint-disable-next-line jest/valid-title
process.platform === 'darwin' || process.platform === 'win32',
'Skip on macos and windows'
)
@ -963,3 +964,69 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
})
})
})
test('theme persists', async ({ page, context }) => {
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)
|> extrude(10, %)
`
)
}, KCL_DEFAULT_LENGTH)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.waitForTimeout(500)
// await page.getByRole('link', { name: 'Settings Settings (tooltip)' }).click()
await expect(page.getByTestId('settings-link')).toBeVisible()
await page.getByTestId('settings-link').click()
// open user settingns
await page.getByRole('radio', { name: 'person User' }).click()
await page.getByTestId('app-theme').selectOption('light')
await page.getByTestId('settings-close-button').click()
const networkToggle = page.getByTestId('network-toggle')
// simulate network down
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Disconnect and reconnect to check the theme persists through a reload
// Expect the network to be down
await expect(networkToggle).toContainText('Offline')
// simulate network up
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
await expect(networkToggle).toContainText('Connected')
await expect(page.getByText('building scene')).not.toBeVisible()
await expect(page, 'expect screenshot to have light theme').toHaveScreenshot({
maxDiffPixels: 100,
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -5,6 +5,7 @@ import {
TestInfo,
BrowserContext,
_electron as electron,
Locator,
} from '@playwright/test'
import { EngineCommand } from 'lang/std/artifactGraph'
import os from 'os'
@ -94,6 +95,8 @@ async function expectCmdLog(page: Page, locatorStr: string, timeout = 5000) {
await expect(page.locator(locatorStr).last()).toBeVisible({ timeout })
}
// Ignoring the lint since I assume someone will want to use this for a test.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function waitForDefaultPlanesToBeVisible(page: Page) {
await page.waitForFunction(
() =>
@ -102,17 +105,21 @@ async function waitForDefaultPlanesToBeVisible(page: Page) {
)
}
async function openKclCodePanel(page: Page) {
const paneLocator = page.getByTestId('code-pane-button')
const ariaSelected = await paneLocator?.getAttribute('aria-pressed')
const isOpen = ariaSelected === 'true'
async function openPane(page: Page, testId: string) {
const locator = page.getByTestId(testId)
await expect(locator).toBeVisible()
const isOpen = (await locator?.getAttribute('aria-pressed')) === 'true'
if (!isOpen) {
await paneLocator.click()
await expect(paneLocator).toHaveAttribute('aria-pressed', 'true')
await locator.click()
await expect(locator).toHaveAttribute('aria-pressed', 'true')
}
}
async function openKclCodePanel(page: Page) {
await openPane(page, 'code-pane-button')
}
async function closeKclCodePanel(page: Page) {
const paneLocator = page.getByTestId('code-pane-button')
const ariaSelected = await paneLocator?.getAttribute('aria-pressed')
@ -125,14 +132,7 @@ async function closeKclCodePanel(page: Page) {
}
async function openDebugPanel(page: Page) {
const debugLocator = page.getByTestId('debug-pane-button')
await expect(debugLocator).toBeVisible()
const isOpen = (await debugLocator?.getAttribute('aria-pressed')) === 'true'
if (!isOpen) {
await debugLocator.click()
await expect(debugLocator).toHaveAttribute('aria-pressed', 'true')
}
await openPane(page, 'debug-pane-button')
}
async function closeDebugPanel(page: Page) {
@ -145,6 +145,28 @@ async function closeDebugPanel(page: Page) {
}
}
async function openFilePanel(page: Page) {
await openPane(page, 'files-pane-button')
}
async function closeFilePanel(page: Page) {
const fileLocator = page.getByTestId('files-pane-button')
await expect(fileLocator).toBeVisible()
const isOpen = (await fileLocator?.getAttribute('aria-pressed')) === 'true'
if (isOpen) {
await fileLocator.click()
await expect(fileLocator).not.toHaveAttribute('aria-pressed', 'true')
}
}
async function openVariablesPane(page: Page) {
await openPane(page, 'variables-pane-button')
}
async function openLogsPane(page: Page) {
await openPane(page, 'logs-pane-button')
}
async function waitForCmdReceive(page: Page, commandType: string) {
return page
.locator(`[data-receive-command-type="${commandType}"]`)
@ -171,7 +193,8 @@ export const wiggleMove = async (
const isElVis = await page.locator(locator).isVisible()
if (isElVis) return
}
const [x1, y1] = [0, Math.sin((tau / steps) * j * freq) * amplitude]
// x1 is 0.
const y1 = Math.sin((tau / steps) * j * freq) * amplitude
const [x2, y2] = [
Math.cos(-ang * deg) * i - Math.sin(-ang * deg) * y1,
Math.sin(-ang * deg) * i + Math.cos(-ang * deg) * y1,
@ -317,6 +340,10 @@ export async function getUtils(page: Page) {
closeKclCodePanel: () => closeKclCodePanel(page),
openDebugPanel: () => openDebugPanel(page),
closeDebugPanel: () => closeDebugPanel(page),
openFilePanel: () => openFilePanel(page),
closeFilePanel: () => closeFilePanel(page),
openVariablesPane: () => openVariablesPane(page),
openLogsPane: () => openLogsPane(page),
openAndClearDebugPanel: async () => {
await openDebugPanel(page)
return clearCommandLogs(page)
@ -452,7 +479,10 @@ export async function getUtils(page: Page) {
return page.evaluate('window.tearDown()')
}
cdpSession?.send('Network.emulateNetworkConditions', networkOptions)
return cdpSession?.send(
'Network.emulateNetworkConditions',
networkOptions
)
},
}
}
@ -539,17 +569,34 @@ export interface Paths {
export const doExport = async (
output: Models['OutputFormat_type'],
page: Page,
isElectron = false
exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown'
): Promise<Paths> => {
if (!isElectron) {
if (exportFrom === 'dropdown') {
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 {
} else if (exportFrom === 'sidebarButton') {
await expect(page.getByTestId('export-pane-button')).toBeVisible()
await page.getByTestId('export-pane-button').click()
} else if (exportFrom === 'commandBar') {
const commandBarButton = page.getByRole('button', { name: 'Commands' })
await expect(commandBarButton).toBeVisible()
// Click the command bar button
await commandBarButton.click()
// Wait for the command bar to appear
const cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible()
const textToCadCommand = page.getByRole('option', {
name: 'floppy disk arrow Export',
})
await expect(textToCadCommand.first()).toBeVisible()
// Click the Text-to-CAD command
await textToCadCommand.first().click()
}
await expect(page.getByTestId('command-bar')).toBeVisible()
@ -577,7 +624,7 @@ export const doExport = async (
const [downloadPromise1, downloadResolve1] = getPromiseAndResolve()
let downloadCnt = 0
if (!isElectron)
if (exportFrom === 'dropdown')
page.on('download', async (download) => {
if (downloadCnt === 0) {
downloadResolve1(download)
@ -585,7 +632,7 @@ export const doExport = async (
downloadCnt++
})
await page.getByRole('button', { name: 'Submit command' }).click()
if (isElectron) {
if (exportFrom === 'sidebarButton' || exportFrom === 'commandBar') {
return {
modelPath: '',
imagePath: '',
@ -649,6 +696,7 @@ export async function tearDown(page: Page, testInfo: TestInfo) {
export async function setup(context: BrowserContext, page: Page) {
await context.addInitScript(
async ({ token, settingsKey, settings, IS_PLAYWRIGHT_KEY }) => {
localStorage.clear()
localStorage.setItem('TOKEN_PERSIST_KEY', token)
localStorage.setItem('persistCode', ``)
localStorage.setItem(settingsKey, settings)
@ -684,6 +732,8 @@ export async function setup(context: BrowserContext, page: Page) {
])
// kill animations, speeds up tests and reduced flakiness
await page.emulateMedia({ reducedMotion: 'reduce' })
await page.reload()
}
export async function setupElectron({
@ -745,3 +795,22 @@ export async function setupElectron({
return { electronApp, page }
}
export async function isOutOfViewInScrollContainer(
element: Locator,
container: Locator
): Promise<boolean> {
const elementBox = await element.boundingBox({ timeout: 5_000 })
const containerBox = await container.boundingBox({ timeout: 5_000 })
let isOutOfView = false
if (elementBox && containerBox)
return (
elementBox.y + elementBox.height > containerBox.y + containerBox.height ||
elementBox.y < containerBox.y ||
elementBox.x + elementBox.width > containerBox.x + containerBox.width ||
elementBox.x < containerBox.x
)
return isOutOfView
}

View File

@ -72,7 +72,7 @@ test.describe('Testing constraints', () => {
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
})
test(`Test remove constraints`, async ({ page }) => {
test(`Remove constraints`, async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',

View File

@ -977,10 +977,6 @@ const part001 = startSketchOn('XZ')
const hoverPos = { x: segmentToDelete.x, y: segmentToDelete.y }
await page.mouse.move(0, 0)
await page.waitForTimeout(1000)
let x = 0,
y = 0
x = hoverPos.x + Math.cos(ang * deg) * 32
y = hoverPos.y - Math.sin(ang * deg) * 32
await page.mouse.move(hoverPos.x, hoverPos.y)
await wiggleMove(
page,

View File

@ -4,7 +4,6 @@ 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)
@ -116,8 +115,7 @@ test.describe('Testing settings', () => {
).not.toBeChecked()
})
// TODO fixme reset doesn't seem to work for color setting
test.fixme('Project and user settings can be reset', async ({ page }) => {
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()
@ -162,6 +160,11 @@ test.describe('Testing settings', () => {
// Click the reset settings button.
await resetButton.click()
await expect(page.getByText('Settings restored to default')).toBeVisible()
await expect(
page.getByText('Settings restored to default')
).not.toBeVisible()
// Verify it is now set to the inherited user value
await expect(themeColorSetting).toHaveValue(settingValues.default)
@ -193,6 +196,10 @@ test.describe('Testing settings', () => {
`Project settings override user settings on desktop`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
test.skip(
process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
)
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
@ -205,7 +212,6 @@ test.describe('Testing settings', () => {
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)

View File

@ -1,7 +1,5 @@
import { test, expect, Page } from '@playwright/test'
import * as fsp from 'fs/promises'
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
import { join } from 'path'
import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
@ -192,7 +190,8 @@ test.describe('Text-to-CAD tests', () => {
await expect(prompt.first()).toBeVisible()
// Type the prompt.
await page.keyboard.type('akjsndladf ghgsssswefiuwq22262664')
const randomPrompt = `aslkdfja;` + Date.now() + `FFFFEIWJF`
await page.keyboard.type(randomPrompt)
await page.waitForTimeout(1000)
await page.keyboard.press('Enter')

View File

@ -0,0 +1,34 @@
import { test, expect } from '@playwright/test'
import { setupElectron, tearDown } from './test-utils'
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Electron user sidebar menu tests', () => {
test(
'User settings has correct shortcut',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
await page.setViewportSize({ width: 1200, height: 500 })
// Open the user sidebar menu.
await page.getByTestId('user-sidebar-toggle').click()
// No space after "User settings" since it's textContent.
const text =
process.platform === 'darwin' ? 'User settings⌘,' : 'User settingsCtrl,'
const userSettingsButton = page.getByTestId('user-settings')
await expect(userSettingsButton).toBeVisible()
await expect(userSettingsButton).toHaveText(text)
await electronApp.close()
}
)
})

View File

@ -631,6 +631,7 @@
"errorno": {
"description": "The error number.",
"format": "int64",
"nullable": true,
"type": "integer"
},
"reason": {
@ -661,6 +662,7 @@
"tar_temp": {
"description": "The target temperature.",
"format": "int64",
"nullable": true,
"type": "integer"
},
"target": {
@ -671,10 +673,8 @@
},
"required": [
"command",
"errorno",
"result",
"sequence_id",
"tar_temp",
"target"
],
"type": "object"
@ -1155,6 +1155,59 @@
],
"type": "object"
},
{
"additionalProperties": true,
"description": "A gcode file.",
"properties": {
"command": {
"enum": [
"gcode_file"
],
"type": "string"
},
"param": {
"description": "The param.",
"nullable": true,
"type": "string"
},
"print_type": {
"description": "The print type.",
"nullable": true,
"type": "string"
},
"reason": {
"allOf": [
{
"$ref": "#/components/schemas/Reason"
}
],
"description": "The reason for the message."
},
"result": {
"allOf": [
{
"$ref": "#/components/schemas/Result"
}
],
"description": "The result of the command."
},
"sequence_id": {
"allOf": [
{
"$ref": "#/components/schemas/SequenceId"
}
],
"description": "The sequence id."
}
},
"required": [
"command",
"reason",
"result",
"sequence_id"
],
"type": "object"
},
{
"additionalProperties": true,
"description": "Project file.",

View File

@ -19,7 +19,7 @@
"@codemirror/search": "^6.5.6",
"@codemirror/state": "^6.4.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@csstools/postcss-oklab-function": "^3.0.16",
"@csstools/postcss-oklab-function": "^4.0.2",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
@ -53,7 +53,7 @@
"react-json-view": "^1.21.3",
"react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2",
"react-router-dom": "^6.23.1",
"react-router-dom": "^6.26.1",
"sketch-helpers": "^0.0.4",
"three": "^0.166.1",
"ua-parser-js": "^1.0.37",
@ -81,22 +81,23 @@
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"fetch:wasm": "./get-latest-wasm-bundle.sh",
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
"build:wasm": "(cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm": "(cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src",
"lint": "eslint --fix src e2e",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
"postinstall": "yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
"make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"electron:start": "electron-forge start",
"electron:package": "electron-forge package",
"electron:make": "electron-forge make",
"electron:publish": "electron-forge publish",
"electron:e2e:local": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron"
"tron:start": "electron-forge start",
"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"
},
"prettier": {
"trailingComma": "es5",
@ -130,17 +131,17 @@
"@electron/fuses": "^1.8.0",
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1",
"@playwright/test": "^1.45.1",
"@playwright/test": "^1.46.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@types/d3-force": "^3.0.10",
"@types/electron": "^1.6.10",
"@types/isomorphic-fetch": "^0.0.39",
"@types/mocha": "^10.0.6",
"@types/node": "^18.19.31",
"@types/node": "^22.4.2",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",
"@types/react": "^18.3.2",
"@types/react": "^18.3.4",
"@types/react-dom": "^18.2.25",
"@types/react-modal": "^3.16.3",
"@types/three": "^0.163.0",
@ -156,7 +157,7 @@
"@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19",
"d3-force": "^3.0.0",
"electron": "^31.2.1",
"electron": "^32.0.1",
"eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",

View File

@ -185,6 +185,8 @@ export function Toolbar({
maybeIconConfig[0].disabled
}
name={maybeIconConfig[0].title}
// aria-description is still in ARIA 1.3 draft.
// eslint-disable-next-line jsx-a11y/aria-props
aria-description={maybeIconConfig[0].description}
onClick={() =>
maybeIconConfig[0].onClick(configCallbackProps)
@ -225,6 +227,8 @@ export function Toolbar({
(!itemConfig.showTitle ? ' !px-0' : '')
}
name={itemConfig.title}
// aria-description is still in ARIA 1.3 draft.
// eslint-disable-next-line jsx-a11y/aria-props
aria-description={itemConfig.description}
aria-pressed={itemConfig.isActive}
disabled={

View File

@ -52,24 +52,21 @@ const DownloadAppBanner = () => {
</a>{' '}
to download the app for the best experience.
</p>
<p className="mt-6">
If you're on Linux and the browser is your only way to use the app,
you can permanently dismiss this banner by{' '}
<a
onClick={() => {
setIsBannerDismissed(true)
settings.send({
type: 'set.app.dismissWebBanner',
data: { level: 'user', value: true },
})
}}
href="/"
className="!text-warn-80 dark:!text-warn-80 dark:hover:!text-warn-70 underline"
>
toggling the App &gt; Dismiss Web Banner setting
</a>
.
</p>
{!navigator?.userAgent.includes('Chrome') && (
<p className="mt-6">
If you want to stay here on the web-app, we currently only support
Chrome. Please use{' '}
<a
href="https://www.google.com/chrome/"
rel="noopener noreferrer"
target="_blank"
className="!text-warn-80 dark:!text-warn-80 dark:hover:!text-warn-70 underline"
>
this link
</a>{' '}
to download it.
</p>
)}
</div>
</Dialog.Panel>
</Dialog>

View File

@ -153,33 +153,34 @@ export const FileMachineProvider = ({
event: EventFrom<typeof fileMachine, 'Rename file'>
) => {
const { oldName, newName, isDir } = event.data
const name = newName ? newName : DEFAULT_FILE_NAME
const name = newName
? newName.endsWith(FILE_EXT) || isDir
? newName
: newName + FILE_EXT
: DEFAULT_FILE_NAME
const oldPath = window.electron.path.join(
context.selectedDirectory.path,
oldName
)
const newDirPath = window.electron.path.join(
const newPath = window.electron.path.join(
context.selectedDirectory.path,
name
)
const newPath =
newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
await window.electron.rename(oldPath, newPath)
window.electron.rename(oldPath, newPath)
if (!file) {
return Promise.reject(new Error('file is not defined'))
}
const currentFilePath = window.electron.path.join(file.path, file.name)
if (oldPath === currentFilePath && project?.path) {
if (oldPath === file.path && project?.path) {
// If we just renamed the current file, navigate to the new path
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)
file.path.replace(oldPath, newPath)
)}`
)
}

View File

@ -464,7 +464,10 @@ export const FileTreeInner = ({
}, [documentHasFocus])
return (
<div className="overflow-auto pb-12 absolute inset-0">
<div
className="overflow-auto pb-12 absolute inset-0"
data-testid="file-pane-scroll-container"
>
<ul
className="m-0 p-0 text-sm"
onClickCapture={(e) => {

View File

@ -23,7 +23,10 @@ export function HelpMenu(props: React.PropsWithChildren) {
return (
<Popover className="relative">
<Popover.Button className="grid p-0 m-0 border-none rounded-full place-content-center">
<Popover.Button
className="grid p-0 m-0 border-none rounded-full place-content-center"
data-testid="help-button"
>
<CustomIcon
name="questionMark"
className="rounded-full w-7 h-7 bg-chalkboard-110 dark:bg-chalkboard-80 text-chalkboard-10"
@ -95,6 +98,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
: PATHS.HOME + PATHS.SETTINGS_KEYBINDINGS
navigate(targetPath)
}}
data-testid="keybindings-button"
>
Keyboard shortcuts
</HelpMenuItem>

View File

@ -11,7 +11,6 @@ import toast from 'react-hot-toast'
import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow'
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
export function LowerRightControls({
children,
@ -25,8 +24,6 @@ export function LowerRightControls({
const linkOverrideClassName =
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
const isTestEnv = window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true'
async function reportbug(event: {
preventDefault: () => void
stopPropagation: () => void
@ -74,7 +71,7 @@ export function LowerRightControls({
rel="noopener noreferrer"
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
>
v{isTestEnv ? '11.22.33' : APP_VERSION}
v{APP_VERSION}
</a>
<a
onClick={reportbug}
@ -96,6 +93,7 @@ export function LowerRightControls({
? filePath + PATHS.SETTINGS + '?tab=project'
: PATHS.HOME + PATHS.SETTINGS
}
data-testid="settings-link"
>
<CustomIcon
name="settings"

View File

@ -63,8 +63,10 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
data: { name: 'Make', groupId: 'modeling' },
})
},
hide: () => machineManager.machineCount() === 0,
hideOnPlatform: 'web',
hide: () => !isDesktop(),
disable: () => {
return machineManager.noMachinesReason()
},
},
]
const filteredActions: SidebarAction[] = sidebarActions.filter(
@ -186,6 +188,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
iconSize: 'md',
}}
onClick={action.action}
disabledText={action.disable?.()}
/>
))}
</ul>
@ -238,6 +241,7 @@ interface ModelingPaneButtonProps
onClick: () => void
paneIsOpen?: boolean
showBadge?: BadgeInfoComputed
disabledText?: string
}
function ModelingPaneButton({
@ -245,6 +249,7 @@ function ModelingPaneButton({
onClick,
paneIsOpen,
showBadge,
disabledText,
...props
}: ModelingPaneButtonProps) {
useHotkeys(paneConfig.keybinding, onClick, {
@ -258,6 +263,8 @@ function ModelingPaneButton({
onClick={onClick}
name={paneConfig.title}
data-testid={paneConfig.id + '-pane-button'}
disabled={disabledText !== undefined}
aria-disabled={disabledText !== undefined}
{...props}
>
<ActionIcon
@ -284,6 +291,7 @@ function ModelingPaneButton({
>
<span className="flex-1">
{paneConfig.title}
{disabledText !== undefined ? ` (${disabledText})` : ''}
{paneIsOpen !== undefined ? ` pane` : ''}
</span>
<kbd className="hotkey text-xs capitalize">
@ -326,4 +334,5 @@ export type SidebarAction = {
action: () => void
hideOnPlatform?: 'desktop' | 'web'
hide?: boolean | (() => boolean)
disable?: () => string | undefined
}

View File

@ -9,7 +9,9 @@ export const NetworkMachineIndicator = ({
}: {
className?: string
}) => {
const machineCount = Object.keys(machineManager.machines).length
const machineCount = machineManager.machineCount()
const reason = machineManager.noMachinesReason()
return isDesktop() ? (
<Popover className="relative">
<Popover.Button
@ -26,7 +28,7 @@ export const NetworkMachineIndicator = ({
</p>
)}
<Tooltip position="top-right" wrapperClassName="ui-open:hidden">
Network machines ({machineCount})
Network machines ({machineCount}) {reason && `: ${reason}`}
</Tooltip>
</Popover.Button>
<Popover.Panel

View File

@ -7,12 +7,29 @@ export default function usePlatform() {
const [platformName, setPlatformName] = useState<Platform>('')
useEffect(() => {
async function getPlatform() {
setPlatformName((window.electron.platform ?? '') as Platform)
function getPlatform(): Platform {
const platform = window.electron.platform ?? ''
// https://nodejs.org/api/process.html#processplatform
switch (platform) {
case 'darwin':
return 'macos'
case 'win32':
return 'windows'
// We don't currently care to distinguish between these.
case 'android':
case 'freebsd':
case 'linux':
case 'openbsd':
case 'sunos':
return 'linux'
default:
console.error('Unknown platform:', platform)
return ''
}
}
if (isDesktop()) {
void getPlatform()
setPlatformName(getPlatform())
} else {
if (navigator.userAgent.indexOf('Mac') !== -1) {
setPlatformName('macos')

View File

@ -2026,7 +2026,7 @@ describe('parsing errors', () => {
expect(result).toBeInstanceOf(KCLError)
const error = result as KCLError
expect(error.kind).toBe('syntax')
expect(error.msg).toBe('Unexpected token')
expect(error.msg).toBe('Unexpected token: (')
expect(error.sourceRanges).toEqual([[27, 28]])
})
})

View File

@ -563,6 +563,7 @@ export function createArrayExpression(
start: 0,
end: 0,
digest: null,
nonCodeMeta: { nonCodeNodes: {}, start: [], digest: null },
elements,
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 180 KiB

View File

@ -19,11 +19,6 @@ import init, {
parse_project_route,
base64_decode,
} from '../wasm-lib/pkg/wasm_lib'
import {
configurationToSettingsPayload,
projectConfigurationToSettingsPayload,
} from 'lib/settings/settingsUtils'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { EngineCommandManager } from './std/engineConnection'
@ -40,6 +35,9 @@ import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { TEST } from 'env'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { err } from 'lib/trap'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { DeepPartial } from 'lib/types'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
@ -570,31 +568,30 @@ export function tomlStringify(toml: any): string | Error {
return toml_stringify(JSON.stringify(toml))
}
export function defaultAppSettings(): Partial<SaveSettingsPayload> {
// Immediately go from Configuration -> Partial<SaveSettingsPayload>
// The returned Rust type is Configuration but it's a lie. Every
// property in that returned object is optional. The Partial<T> essentially
// brings that type in-line with that definition.
return configurationToSettingsPayload(default_app_settings())
export function defaultAppSettings(): DeepPartial<Configuration> | Error {
return default_app_settings()
}
export function parseAppSettings(toml: string): Partial<SaveSettingsPayload> {
const parsed = parse_app_settings(toml)
return configurationToSettingsPayload(parsed)
export function parseAppSettings(
toml: string
): DeepPartial<Configuration> | Error {
return parse_app_settings(toml)
}
export function defaultProjectSettings(): Partial<SaveSettingsPayload> {
return projectConfigurationToSettingsPayload(default_project_settings())
export function defaultProjectSettings():
| DeepPartial<ProjectConfiguration>
| Error {
return default_project_settings()
}
export function parseProjectSettings(
toml: string
): Partial<SaveSettingsPayload> {
return projectConfigurationToSettingsPayload(parse_project_settings(toml))
): DeepPartial<ProjectConfiguration> | Error {
return parse_project_settings(toml)
}
export function parseProjectRoute(
configuration: Partial<SaveSettingsPayload>,
configuration: DeepPartial<Configuration>,
route_str: string
): ProjectRoute | Error {
return parse_project_route(JSON.stringify(configuration), route_str)

View File

@ -154,10 +154,6 @@ export function buildCommandArgument<
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
if (arg.inputType === 'options') {
if (!(arg.options || arg.optionsFromContext)) {
throw new Error('Options must be provided for options input type')
}
return {
inputType: arg.inputType,
...baseCommandArgument,

View File

@ -3,11 +3,9 @@ import { Models } from '@kittycad/lib'
import { Project } from 'wasm-lib/kcl/bindings/Project'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import {
defaultAppSettings,
tomlStringify,
parseAppSettings,
parseProjectSettings,
} from 'lang/wasm'
@ -18,6 +16,9 @@ import {
PROJECT_SETTINGS_FILE_NAME,
SETTINGS_FILE_NAME,
} from './constants'
import { DeepPartial } from './types'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export { parseProjectRoute } from 'lang/wasm'
export async function renameProjectDirectory(
@ -61,10 +62,13 @@ export async function renameProjectDirectory(
}
export async function ensureProjectDirectoryExists(
config: Partial<SaveSettingsPayload>
config: DeepPartial<Configuration>
): Promise<string | undefined> {
const projectDir = config.app?.projectDirectory
const projectDir =
config.settings?.app?.project_directory ||
config.settings?.project?.directory
if (!projectDir) {
console.error('projectDir is falsey', config)
return Promise.reject(new Error('projectDir is falsey'))
}
try {
@ -81,12 +85,13 @@ export async function ensureProjectDirectoryExists(
export async function createNewProjectDirectory(
projectName: string,
initialCode?: string,
configuration?: Partial<SaveSettingsPayload>
configuration?: DeepPartial<Configuration> | Error
): Promise<Project> {
if (!configuration) {
configuration = await readAppSettingsFile()
}
if (err(configuration)) return Promise.reject(configuration)
const mainDir = await ensureProjectDirectoryExists(configuration)
if (!projectName) {
@ -124,11 +129,13 @@ export async function createNewProjectDirectory(
}
export async function listProjects(
configuration?: Partial<SaveSettingsPayload>
configuration?: DeepPartial<Configuration> | Error
): Promise<Project[]> {
if (configuration === undefined) {
configuration = await readAppSettingsFile()
}
if (err(configuration)) return Promise.reject(configuration)
const projectDir = await ensureProjectDirectoryExists(configuration)
const projects = []
if (!projectDir) return Promise.reject(new Error('projectDir was falsey'))
@ -179,7 +186,7 @@ const collectAllFilesRecursiveFrom = async (path: string) => {
return Promise.reject(new Error(`Path ${path} is not a directory`))
}
const pathParts = path.split('/')
const pathParts = path.split(window.electron.path.sep)
let entry: FileEntry = {
name: pathParts.slice(-1)[0],
path,
@ -358,10 +365,9 @@ export async function getProjectInfo(projectPath: string): Promise<Project> {
// Write project settings file.
export async function writeProjectSettingsFile(
projectPath: string,
configuration: Partial<SaveSettingsPayload>
tomlStr: string
): Promise<void> {
const projectSettingsFilePath = await getProjectSettingsFilePath(projectPath)
const tomlStr = tomlStringify({ settings: configuration })
if (err(tomlStr)) return Promise.reject(tomlStr)
return window.electron.writeFile(projectSettingsFilePath, tomlStr)
}
@ -407,13 +413,16 @@ const getProjectSettingsFilePath = async (projectPath: string) => {
}
export const getInitialDefaultDir = async () => {
if (!window.electron) {
return ''
}
const dir = await window.electron.getPath('documents')
return window.electron.path.join(dir, PROJECT_FOLDER)
}
export const readProjectSettingsFile = async (
projectPath: string
): Promise<Partial<SaveSettingsPayload>> => {
): Promise<DeepPartial<ProjectConfiguration>> => {
let settingsPath = await getProjectSettingsFilePath(projectPath)
// Check if this file exists.
@ -428,6 +437,9 @@ export const readProjectSettingsFile = async (
const configToml = await window.electron.readFile(settingsPath)
const configObj = parseProjectSettings(configToml)
if (err(configObj)) {
return Promise.reject(configObj)
}
return configObj
}
@ -438,23 +450,25 @@ export const readAppSettingsFile = async () => {
} catch (e) {
if (e === 'ENOENT') {
const config = defaultAppSettings()
if (!config.app) {
if (err(config)) return Promise.reject(config)
if (!config.settings?.app)
return Promise.reject(new Error('config.app is falsey'))
}
config.app.projectDirectory = await getInitialDefaultDir()
config.settings.app.project_directory = await getInitialDefaultDir()
return config
}
}
const configToml = await window.electron.readFile(settingsPath)
const configObj = parseAppSettings(configToml)
if (err(configObj)) {
return Promise.reject(configObj)
}
return configObj
}
export const writeAppSettingsFile = async (
config: Partial<SaveSettingsPayload>
) => {
export const writeAppSettingsFile = async (tomlStr: string) => {
const appSettingsFilePath = await getAppSettingsFilePath()
const tomlStr = tomlStringify({ settings: config })
if (err(tomlStr)) return Promise.reject(tomlStr)
return window.electron.writeFile(appSettingsFilePath, tomlStr)
}

View File

@ -38,6 +38,9 @@ const bracket = startSketchOn('XY')
tags: [getPreviousAdjacentEdge(outerEdge)]
}, %)`
/**
* @throws Error if the search text is not found in the example code.
*/
function findLineInExampleCode({
searchText,
example = bracket,
@ -48,6 +51,8 @@ function findLineInExampleCode({
const lines = example.split('\n')
const lineNumber = lines.findIndex((l) => l.includes(searchText)) + 1
if (lineNumber === 0) {
// We are exporting a constant, so we don't want to return an Error.
// eslint-disable-next-line suggest-no-throw/suggest-no-throw
throw new Error(
`Could not find the line with search text "${searchText}" in the example code. Was it removed?`
)

View File

@ -65,6 +65,13 @@ export async function exportMake(data: ArrayBuffer): Promise<Response | null> {
console.log('response', response)
if (!response.ok) {
console.error('Error exporting', response)
const text = await response.text()
toast.error('Error exporting: ' + response.statusText + ' ' + text)
return null
}
return response
} catch (error) {
console.error('Error exporting', error)

View File

@ -271,7 +271,7 @@ export interface components {
* Format: int64
* @description The error number.
*/
errorno: number
errorno?: number | null
/** @description The reason for the message. */
reason?: components['schemas']['Reason'] | null
/** @description The result of the command. */
@ -282,7 +282,7 @@ export interface components {
* Format: int64
* @description The target temperature.
*/
tar_temp: number
tar_temp?: number | null
/**
* Format: int64
* @description The target.
@ -523,6 +523,22 @@ export interface components {
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'gcode_file'
/** @description The param. */
param?: string | null
/** @description The print type. */
print_type?: string | null
/** @description The reason for the message. */
reason: components['schemas']['Reason']
/** @description The result of the command. */
result: components['schemas']['Result']
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'project_file'

View File

@ -51,6 +51,19 @@ export class MachineManager {
return this._machineApiIp
}
// Get the reason message for why there are no machines.
noMachinesReason(): string | undefined {
if (this.machineCount() > 0) {
return undefined
}
if (this.machineApiIp === null) {
return 'Machine API server was not discovered'
}
return 'Machine API server was discovered, but no machines are available'
}
get currentMachine(): components['schemas']['Machine'] | null {
return this._currentMachine
}

View File

@ -4,9 +4,10 @@ import { isDesktop } from './isDesktop'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { parseProjectRoute, readAppSettingsFile } from './desktop'
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
import { SaveSettingsPayload } from './settings/settingsTypes'
import { err } from 'lib/trap'
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
import { DeepPartial } from './types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => {
@ -39,7 +40,7 @@ export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${
export async function getProjectMetaByRouteId(
id?: string,
configuration?: Partial<SaveSettingsPayload> | Error
configuration?: DeepPartial<Configuration> | Error
): Promise<ProjectRoute | undefined> {
if (!id) return undefined

View File

@ -10,7 +10,7 @@ import {
} from 'lib/constants'
import { loadAndValidateSettings } from './settings/settingsUtils'
import makeUrlPathRelative from './makeUrlPathRelative'
import { codeManager, kclManager } from 'lib/singletons'
import { codeManager } from 'lib/singletons'
import { fileSystemManager } from 'lang/std/fileSystemManager'
import {
getProjectInfo,
@ -107,8 +107,6 @@ export const fileLoader: LoaderFunction = async (
// the file system and not the editor.
codeManager.updateCurrentFilePath(current_file_path)
codeManager.updateCodeStateEditor(code)
// We don't want to call await on execute code since we don't want to block the UI
kclManager.executeCode(true)
}
// Set the file system manager to the project path
@ -125,14 +123,22 @@ export const fileLoader: LoaderFunction = async (
default_file: project_path,
}
const maybeProjectInfo = isDesktop()
? await getProjectInfo(project_path)
: null
console.log('maybeProjectInfo', {
maybeProjectInfo,
defaultProjectData,
projectPathData,
})
const projectData: IndexLoaderData = {
code,
project: isDesktop()
? (await getProjectInfo(project_path)) ?? defaultProjectData
: defaultProjectData,
project: maybeProjectInfo ?? defaultProjectData,
file: {
name: current_file_name || '',
path: current_file_path?.split('/').slice(0, -1).join('/') ?? '',
path: current_file_path || '',
children: [],
},
}

View File

@ -21,6 +21,7 @@ import {
} from 'lib/desktop'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { DeepPartial } from 'lib/types'
/**
* Convert from a rust settings struct into the JS settings struct.
@ -28,8 +29,8 @@ import { BROWSER_PROJECT_NAME } from 'lib/constants'
* for hiding and showing settings.
**/
export function configurationToSettingsPayload(
configuration: Configuration
): Partial<SaveSettingsPayload> {
configuration: DeepPartial<Configuration>
): DeepPartial<SaveSettingsPayload> {
return {
app: {
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
@ -66,8 +67,8 @@ export function configurationToSettingsPayload(
}
export function projectConfigurationToSettingsPayload(
configuration: ProjectConfiguration
): Partial<SaveSettingsPayload> {
configuration: DeepPartial<ProjectConfiguration>
): DeepPartial<SaveSettingsPayload> {
return {
app: {
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
@ -106,7 +107,7 @@ function localStorageProjectSettingsPath() {
}
export function readLocalStorageAppSettingsFile():
| Partial<SaveSettingsPayload>
| DeepPartial<Configuration>
| Error {
// TODO: Remove backwards compatibility after a few releases.
let stored =
@ -132,7 +133,7 @@ export function readLocalStorageAppSettingsFile():
}
function readLocalStorageProjectSettingsFile():
| Partial<SaveSettingsPayload>
| DeepPartial<ProjectConfiguration>
| Error {
// TODO: Remove backwards compatibility after a few releases.
let stored = localStorage.getItem(localStorageProjectSettingsPath()) ?? ''
@ -156,7 +157,7 @@ function readLocalStorageProjectSettingsFile():
export interface AppSettings {
settings: ReturnType<typeof createSettings>
configuration: Partial<SaveSettingsPayload>
configuration: DeepPartial<Configuration>
}
export async function loadAndValidateSettings(
@ -175,7 +176,11 @@ export async function loadAndValidateSettings(
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
const settings = createSettings()
setSettingsAtLevel(settings, 'user', appSettingsPayload)
setSettingsAtLevel(
settings,
'user',
configurationToSettingsPayload(appSettingsPayload)
)
// Load the project settings if they exist
if (projectPath) {
@ -187,11 +192,18 @@ export async function loadAndValidateSettings(
return Promise.reject(new Error('Invalid project settings'))
const projectSettingsPayload = projectSettings
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
setSettingsAtLevel(
settings,
'project',
projectConfigurationToSettingsPayload(projectSettingsPayload)
)
}
// Return the settings object
return { settings, configuration: appSettingsPayload }
return {
settings,
configuration: appSettingsPayload,
}
}
export async function saveSettings(
@ -204,21 +216,14 @@ export async function saveSettings(
// Get the user settings.
const jsAppSettings = getChangedSettingsAtLevel(allSettings, 'user')
const tomlString = tomlStringify({ settings: jsAppSettings })
if (err(tomlString)) return
// Parse this as a Configuration.
const appSettings = parseAppSettings(tomlString)
if (err(appSettings)) return
const tomlString2 = tomlStringify({ settings: appSettings })
if (err(tomlString2)) return
const appTomlString = tomlStringify({ settings: jsAppSettings })
if (err(appTomlString)) return
// Write the app settings.
if (onDesktop) {
await writeAppSettingsFile(appSettings)
await writeAppSettingsFile(appTomlString)
} else {
localStorage.setItem(localStorageAppSettingsPath(), tomlString2)
localStorage.setItem(localStorageAppSettingsPath(), appTomlString)
}
if (!projectPath) {
@ -231,19 +236,11 @@ export async function saveSettings(
const projectTomlString = tomlStringify({ settings: jsProjectSettings })
if (err(projectTomlString)) return
// Parse this as a Configuration.
const projectSettings = parseProjectSettings(projectTomlString)
if (err(projectSettings)) return
const tomlStr = tomlStringify(projectSettings)
if (err(tomlStr)) return
// Write the project settings.
if (onDesktop) {
await writeProjectSettingsFile(projectPath, projectSettings)
await writeProjectSettingsFile(projectPath, projectTomlString)
} else {
localStorage.setItem(localStorageProjectSettingsPath(), tomlStr)
localStorage.setItem(localStorageProjectSettingsPath(), projectTomlString)
}
}

View File

@ -95,3 +95,9 @@ export function isEnumMember<T extends Record<string, unknown>>(
) {
return Object.values(e).includes(v)
}
// utility type to make all *nested* object properties optional
// https://www.geodev.me/blog/deeppartial-in-typescript
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}

View File

@ -131,8 +131,6 @@ ipcMain.handle('kittycad', (event, data) => {
)(data.args)
})
const SERVICE_NAME = '_machine-api._tcp.local.'
ipcMain.handle('find_machine_api', () => {
const timeoutAfterMs = 5000
return new Promise((resolve, reject) => {
@ -144,8 +142,19 @@ ipcMain.handle('find_machine_api', () => {
resolve(null)
})
console.log('Looking for machine API...')
bonjourEt.find({ type: SERVICE_NAME }, (service: Service) => {
resolve(service.fqdn)
})
bonjourEt.find(
{ protocol: 'tcp', type: 'machine-api' },
(service: Service) => {
console.log('Found machine API!', JSON.stringify(service))
if (!service.addresses || service.addresses?.length === 0) {
console.log('No addresses found for machine API!')
return resolve(null)
}
const ip = service.addresses[0]
const port = service.port
// We want to return the ip address of the machine API.
resolve(`${ip}:${port}`)
}
)
})
})

View File

@ -145,7 +145,7 @@ function OnboardingIntroductionInner() {
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
<h1 className="flex flex-wrap items-center gap-4 text-3xl font-bold">
<img
src={`./zma-logomark${getLogoTheme()}.svg`}
src={`${isDesktop() ? '.' : ''}/zma-logomark${getLogoTheme()}.svg`}
alt={APP_NAME}
className="h-20 max-w-full"
/>

View File

@ -13,11 +13,18 @@ import { AllSettingsFields } from 'components/Settings/AllSettingsFields'
import { AllKeybindingsFields } from 'components/Settings/AllKeybindingsFields'
import { KeybindingsSectionsList } from 'components/Settings/KeybindingsSectionsList'
import { isDesktop } from 'lib/isDesktop'
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
import { NODE_ENV } from 'env'
export const APP_VERSION = isDesktop()
? // @ts-ignore
window.electron.packageJson.version
: 'main'
const isTestEnv = window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true'
export const APP_VERSION =
isTestEnv && NODE_ENV === 'development'
? '11.22.33'
: isDesktop()
? // @ts-ignore
window.electron.packageJson.version
: 'main'
export const Settings = () => {
const navigate = useNavigate()

View File

@ -169,7 +169,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -180,7 +180,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -191,7 +191,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -443,7 +443,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -643,7 +643,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -654,7 +654,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -709,7 +709,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
"synstructure",
]
@ -724,7 +724,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.23"
version = "0.1.24"
dependencies = [
"Inflector",
"anyhow",
@ -738,7 +738,7 @@ dependencies = [
"rustfmt-wrapper",
"serde",
"serde_tokenstream",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -749,7 +749,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -776,7 +776,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -948,7 +948,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -1038,7 +1038,7 @@ dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -1397,7 +1397,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"anyhow",
"approx",
@ -1462,12 +1462,12 @@ dependencies = [
"pretty_assertions",
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
name = "kcl-test-server"
version = "0.1.7"
version = "0.1.8"
dependencies = [
"anyhow",
"hyper",
@ -1841,7 +1841,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.3",
"structmeta",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -1894,7 +1894,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -2058,7 +2058,7 @@ dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -2071,7 +2071,7 @@ dependencies = [
"proc-macro2",
"pyo3-build-config",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -2533,7 +2533,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -2607,7 +2607,7 @@ checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -2618,7 +2618,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -2642,7 +2642,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -2663,7 +2663,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -2800,7 +2800,7 @@ dependencies = [
"proc-macro2",
"quote",
"structmeta-derive",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -2811,7 +2811,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -2855,9 +2855,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.74"
version = "2.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
dependencies = [
"proc-macro2",
"quote",
@ -2878,7 +2878,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -2985,7 +2985,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -3056,9 +3056,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.39.2"
version = "1.39.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
dependencies = [
"backtrace",
"bytes",
@ -3080,7 +3080,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -3233,7 +3233,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -3261,7 +3261,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -3338,7 +3338,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
"termcolor",
]
@ -3502,7 +3502,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]
@ -3563,7 +3563,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
"wasm-bindgen-shared",
]
@ -3598,7 +3598,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -3924,7 +3924,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.74",
"syn 2.0.75",
]
[[package]]

View File

@ -17,7 +17,7 @@ gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad.workspace = true
serde_json = "1.0.125"
tokio = { version = "1.39.2", features = ["sync"] }
tokio = { version = "1.39.3", features = ["sync"] }
toml = "0.8.19"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91"
@ -30,7 +30,7 @@ image = { version = "0.25.1", default-features = false, features = ["png"] }
kittycad = { workspace = true, default-features = true }
pretty_assertions = "1.4.0"
reqwest = { version = "0.11.26", default-features = false }
tokio = { version = "1.39.2", features = ["rt-multi-thread", "macros", "time"] }
tokio = { version = "1.39.3", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.8"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.23"
version = "0.1.24"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -20,7 +20,7 @@ quote = "1"
regex = "1.10"
serde = { version = "1.0.208", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.74", features = ["full"] }
syn = { version = "2.0.75", features = ["full"] }
[dev-dependencies]
anyhow = "1.0.86"

View File

@ -15,7 +15,7 @@ databake = "0.1.8"
kcl-lib = { path = "../kcl" }
proc-macro2 = "1"
quote = "1"
syn = { version = "2.0.74", features = ["full"] }
syn = { version = "2.0.75", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1.4.0"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.7"
version = "0.1.8"
edition = "2021"
license = "MIT"
@ -12,4 +12,4 @@ kcl-lib = { version = "0.2", path = "../kcl" }
pico-args = "0.5.0"
serde = { version = "1.0.208", features = ["derive"] }
serde_json = "1.0.125"
tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1.39.3", features = ["macros", "rt-multi-thread"] }

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.5"
version = "0.2.6"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -20,7 +20,7 @@ clap = { version = "4.5.16", default-features = false, optional = true }
convert_case = "0.6.0"
dashmap = "6.0.1"
databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.23", path = "../derive-docs" }
derive-docs = { version = "0.1.24", path = "../derive-docs" }
form_urlencoded = "1.2.1"
futures = { version = "0.3.30" }
git_rev = "0.1.0"
@ -50,7 +50,7 @@ zip = { version = "2.0.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" }
tokio = { version = "1.39.2", features = ["sync", "time"] }
tokio = { version = "1.39.3", features = ["sync", "time"] }
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.42"
@ -59,7 +59,7 @@ web-sys = { version = "0.3.69", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
approx = "0.5"
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
tokio = { version = "1.39.2", features = ["full"] }
tokio = { version = "1.39.3", features = ["full"] }
tokio-tungstenite = { version = "0.23.1", features = ["rustls-tls-native-roots"] }
tower-lsp = { version = "0.20.0", features = ["proposed"] }

View File

@ -1149,6 +1149,15 @@ pub enum NonCodeValue {
NewLine,
}
impl NonCodeValue {
fn should_cause_array_newline(&self) -> bool {
match self {
Self::InlineComment { .. } => false,
Self::Shebang { .. } | Self::BlockComment { .. } | Self::NewLineBlockComment { .. } | Self::NewLine => true,
}
}
}
#[derive(Debug, Default, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
@ -1160,6 +1169,18 @@ pub struct NonCodeMeta {
pub digest: Option<Digest>,
}
impl NonCodeMeta {
/// Does this contain anything?
pub fn is_empty(&self) -> bool {
self.non_code_nodes.is_empty() && self.start.is_empty()
}
/// How many non-code values does this have?
pub fn non_code_nodes_len(&self) -> usize {
self.non_code_nodes.values().map(|x| x.len()).sum()
}
}
// implement Deserialize manually because we to force the keys of non_code_nodes to be usize
// and by default the ts type { [statementIndex: number]: NonCodeNode } serializes to a string i.e. "0", "1", etc.
impl<'de> Deserialize<'de> for NonCodeMeta {
@ -2224,11 +2245,13 @@ impl From<PipeSubstitution> for Expr {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase", tag = "type")]
pub struct ArrayExpression {
pub start: usize,
pub end: usize,
pub elements: Vec<Expr>,
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
pub digest: Option<Digest>,
}
@ -2247,6 +2270,7 @@ impl ArrayExpression {
start: 0,
end: 0,
elements,
non_code_meta: Default::default(),
digest: None,
}
}
@ -2280,38 +2304,70 @@ impl ArrayExpression {
}
fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String {
let flat_recast = format!(
"[{}]",
self.elements
.iter()
.map(|el| el.recast(options, 0, false))
.collect::<Vec<String>>()
.join(", ")
);
let max_array_length = 40;
if flat_recast.len() > max_array_length {
let inner_indentation = if is_in_pipe {
options.get_indentation_offset_pipe(indentation_level + 1)
} else {
options.get_indentation(indentation_level + 1)
};
format!(
"[\n{}{}\n{}]",
inner_indentation,
self.elements
.iter()
.map(|el| el.recast(options, indentation_level, is_in_pipe))
.collect::<Vec<String>>()
.join(format!(",\n{}", inner_indentation).as_str()),
if is_in_pipe {
options.get_indentation_offset_pipe(indentation_level)
// Reconstruct the order of items in the array.
// An item can be an element (i.e. an expression for a KCL value),
// or a non-code item (e.g. a comment)
let num_items = self.elements.len() + self.non_code_meta.non_code_nodes_len();
let mut elems = self.elements.iter();
let mut found_line_comment = false;
let mut format_items: Vec<_> = (0..num_items)
.flat_map(|i| {
if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
noncode
.iter()
.map(|nc| {
found_line_comment |= nc.value.should_cause_array_newline();
nc.format("")
})
.collect::<Vec<_>>()
} else {
options.get_indentation(indentation_level)
},
)
} else {
flat_recast
let el = elems.next().unwrap();
let s = format!("{}, ", el.recast(options, 0, false));
vec![s]
}
})
.collect();
// Format these items into a one-line array.
if let Some(item) = format_items.last_mut() {
if let Some(norm) = item.strip_suffix(", ") {
*item = norm.to_owned();
}
}
let format_items = format_items; // Remove mutability
let flat_recast = format!("[{}]", format_items.join(""));
// We might keep the one-line representation, if it's short enough.
let max_array_length = 40;
let multi_line = flat_recast.len() > max_array_length || found_line_comment;
if !multi_line {
return flat_recast;
}
// Otherwise, we format a multi-line representation.
let inner_indentation = if is_in_pipe {
options.get_indentation_offset_pipe(indentation_level + 1)
} else {
options.get_indentation(indentation_level + 1)
};
let formatted_array_lines = format_items
.iter()
.map(|s| {
format!(
"{inner_indentation}{}{}",
if let Some(x) = s.strip_suffix(" ") { x } else { s },
if s.ends_with('\n') { "" } else { "\n" }
)
})
.collect::<Vec<String>>()
.join("")
.to_owned();
let end_indent = if is_in_pipe {
options.get_indentation_offset_pipe(indentation_level)
} else {
options.get_indentation(indentation_level)
};
format!("[\n{formatted_array_lines}{end_indent}]")
}
/// Returns a hover value that includes the given character position.
@ -5838,6 +5894,103 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
}
}
#[test]
fn recast_array_with_comments() {
use winnow::Parser;
for (i, (input, expected, reason)) in [
(
"\
[
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
]",
"\
[
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20
]",
"preserves multi-line arrays",
),
(
"\
[
1,
// 2,
3
]",
"\
[
1,
// 2,
3
]",
"preserves comments",
),
(
"\
[
1,
2,
// 3
]",
"\
[
1,
2,
// 3
]",
"preserves comments at the end of the array",
),
]
.into_iter()
.enumerate()
{
let tokens = crate::token::lexer(input).unwrap();
let expr = crate::parser::parser_impl::array_elem_by_elem.parse(&tokens).unwrap();
assert_eq!(
expr.recast(&FormatOptions::new(), 0, false),
expected,
"failed test {i}, which is testing that recasting {reason}"
);
}
}
#[test]
fn required_params() {
for (i, (test_name, expected, function_expr)) in [
@ -6039,7 +6192,7 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([57, 59])], message: "Unexpected token" }"#
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([57, 59])], message: "Unexpected token: |>" }"#
);
}

View File

@ -2898,7 +2898,7 @@ let notTagIdentifier = !myTag";
// a runtime error instead.
parse_execute(code10).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Syntax(KclErrorDetails {
message: "Unexpected token".to_owned(),
message: "Unexpected token: !".to_owned(),
source_ranges: vec![SourceRange([14, 15])],
})
);
@ -2911,7 +2911,7 @@ let notPipeSub = 1 |> identity(!%))";
// a runtime error instead.
parse_execute(code11).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Syntax(KclErrorDetails {
message: "Unexpected token".to_owned(),
message: "Unexpected token: |>".to_owned(),
source_ranges: vec![SourceRange([54, 56])],
})
);

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