Compare commits

...

104 Commits

Author SHA1 Message Date
cf4655ff6a Test quote change windows 2024-12-06 14:10:34 -05:00
e338c39eab Fixmes, fixmes everywhere 2024-12-06 14:07:09 -05:00
d1d98897dc Merge branch 'main' into move-tests-to-electon 2024-12-06 13:20:47 -05:00
98b4aaea84 2024-12-06 13:19:31 -05:00
30d365aeb3 Module/import upgrades (#4677)
* Parse more import syntax

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Remove unnecessary Vec from VariableDeclaration

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Parse export import

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Factor out an execution module

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* imports: constants, globs, export import

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* test fixups

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-06 13:16:04 -05:00
1faad75462 Hopefully help CI flake 2024-12-06 13:12:45 -05:00
7af62399ac Change vite-plugin-eslint to maintained package (#4645)
* Change vite-plugin-eslint to maintained package

* Add .eslintcache to ignores
2024-12-06 12:28:58 -05:00
496a6c2768 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-06 17:07:18 +00:00
5bb1bf520e Fix flake in double click editor 2024-12-06 12:01:50 -05:00
a5377f50ff A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-06 16:32:20 +00:00
2fffa7e6d8 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-06 16:28:02 +00:00
98c50eacc5 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-06 16:27:02 +00:00
8f5c8447ea Stop snapshots of exports (already test in e2e) 2024-12-06 11:23:36 -05:00
ded9f9b89f Upgrade to playwright 1.49.0 to fix type errors 2024-12-06 11:19:30 -05:00
086bf9b736 Merge branch 'main' into move-tests-to-electon 2024-12-06 10:56:42 -05:00
441d957228 start of cache: don't re-execute on whitespace / top level code comment changes (#4663)
* start

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

working for whitespace

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

pull thru

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

fix wasm

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

pull thru to js start

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

actually use the cache in ts

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

rust owns clearing the scene

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

fixes

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

empty

stupid log

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

updates

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

* updates

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

fix tests

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

updatez

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

updates

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

save the state

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

save the state

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

updates

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

use the old memory

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

cleanup to use the old exec state

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

fices

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>

fmt

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>

updates

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

cleanup

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

fixes

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

* rebase and compile

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

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

* fix the lsp to use the cache

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

* add comment

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

* use a global static instead;

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

* fix rust test

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

* cleanup more

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

* cleanups

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

* cleanup the api even more

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

* updates

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*

* bust the cache on unit changes

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

* updates

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

* updates

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*

* stupid codespell

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-12-06 03:51:06 +00:00
9e57034873 AST: Allow KCL fn params to have defaults and labels (#4676)
Pure refactor, should not change any behaviour.

Previously, optional parameters in KCL function calls always set the parameter to KclNone. 

As of this PR, they can be set to KCL literals in addition to KCL none. However the parser does not actually ever use this (that'll be in a follow-up PR).

Also adds a `labeled: bool` to all parameters, which is always true. But it lays the groundwork for the unlabeled first parameter in a follow-up PR.
2024-12-06 03:04:40 +00:00
eb96d6539c Surface warnings to frontend and LSP (#4603)
* Send multiple errors and warnings to the frontend and LSP

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor the parser to use CompilationError for parsing errors rather than KclError

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactoring: move CompilationError, etc.

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Integrate compilation errors with the frontend and CodeMirror

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Fix tests

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Review comments

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Fix module id/source range stuff

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* More test fixups

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-06 13:57:31 +13:00
513c76ecc8 KCL: Remove stdlib written in KCL (#4673)
We don't have any of these, and I don't think it's
worth the complexity. The goal was to let us write
KCL stdlib functions in KCL not Rust. But who cares
really. We can always put this back if we need it.
2024-12-05 23:59:37 +00:00
51d9449280 Fix broken test from previous PR (#4674)
* Fix broken test from previous PR

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

* void

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-05 23:39:40 +00:00
129e07f059 Fix loft test 2024-12-05 17:22:18 -05:00
137e528944 Try CI again 2024-12-05 17:16:42 -05:00
b9a07c9393 Merge branch 'main' into move-tests-to-electon 2024-12-05 17:01:37 -05:00
f18f975e1c Add an f-ton of shit because playwright doesnt want S P R E A D 2024-12-05 17:01:24 -05:00
6cd1af23e6 Look at this (photo)Graph *in the voice of Nickelback* 2024-12-05 21:47:08 +00:00
cc247ace0e Remove to-electron script 2024-12-05 16:38:08 -05:00
68bcee9298 Correct all tsc errors 2024-12-05 16:37:56 -05:00
6366bc4766 KCL: Keyword function calls for stdlib (#4647)
Part of https://github.com/KittyCAD/modeling-app/issues/4600

Adds support for keyword arguments to the stdlib, and calling stdlib functions with keyword arguments.

So far, I've changed one function: `rem`. Previously you would have used `rem(7, 2)` but now it's `rem(7, divisor: 2)`.

This is a proof-of-concept. If it's approved, we will:

1. Support closures with keyword arguments, and calling them
2. Move the rest of the stdlib to use kw arguments
2024-12-05 14:27:51 -06:00
7a21918223 Cleaner nightly release notes (#4668)
* Bump: Improve and fix nightly release notes

* Clean up after manual push

* Consistency tweak
2024-12-05 15:14:58 -05:00
8072f1db63 Add support for line comments in playwright-secrets.env (#4671) 2024-12-05 19:45:33 +00:00
18e1855fa9 Bump indexmap from 2.6.0 to 2.7.0 in /src/wasm-lib (#4622)
Bumps [indexmap](https://github.com/indexmap-rs/indexmap) from 2.6.0 to 2.7.0.
- [Changelog](https://github.com/indexmap-rs/indexmap/blob/master/RELEASES.md)
- [Commits](https://github.com/indexmap-rs/indexmap/compare/2.6.0...2.7.0)

---
updated-dependencies:
- dependency-name: indexmap
  dependency-type: direct:production
  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-12-05 17:44:35 +00:00
7be53c7d4a Bump KCL, KCL test server, derive-docs (#4670) 2024-12-05 17:34:43 +00:00
2bf20988ef Fix to never have undefined iteration order and lint against it (#4665) 2024-12-05 17:09:35 +00:00
1495cc6d18 Fix default planes to be created in deterministic order (#4664)
* Fix default planes to be created in deterministic order

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

* Trigger CI

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

* Trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-05 16:04:04 +00:00
f876e6ca3c Bump and release kcl-lib 0.2.28 (#4669)
bump kcl version
2024-12-05 15:03:55 +00:00
60a0c811ab Move parsing files around (#4626)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-05 17:56:49 +13:00
cab0c1e6a1 Add list of commits as changelog between nightly builds (#4654) 2024-12-04 19:06:17 -05:00
417d720b22 Point-and-click Loft (#4605)
* WIP: experimenting with Loft UI
Relates to #4470

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

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

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

* Add selection guard

* Working loft for two sketches in the right hardcoded order

* First pass at handling more than 2 sketches

* WIP selections

* WIP selections

* More checks

* Appends the loft line after the 'last' sketch in the code

* Clean up

* Enable multiple selections after the button click

* First point-click loft test (not working locally, loft gets inserted at the wrong place)

* Lint

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

* Clean up and working pw test

* Add test for doesSceneHaveSweepableSketch with count = 2

* Clean up loftSketches function

* Add pw test for preselected sketches

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

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

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

* Trigger CI

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

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

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

* Move to fromPromise-based Actor

* Move error logic out of loftSketches, fix pw tests

* Remove comments

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

* Trigger CI

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

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

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

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

* Trigger CI

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

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

* Fix typo

* Revert snapshots

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

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

* Trigger CI

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

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

* Trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-04 22:24:16 +00:00
77293952c0 fix: upon entering sketch mode, axis do not rotate. (#4572)
* fix: upon entering sketch mode, axis do not rotate.

* fix: camera settings has the up vector to set, this should now work
2024-12-04 15:57:17 -06:00
ea3d604b73 Allow standard planes to appear in the artifact graph (#4594) 2024-12-04 21:06:02 +00:00
023a659491 Make some fields of lint::Discovered public again; update kittycad (#4658)
* Make some fields of lint::Discovered public again; update kittycad

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Empty commit to try to unstick CI

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-05 10:05:23 +13:00
dd3a2b14f9 Bump hashbrown from 0.15.0 to 0.15.2 (#4659) 2024-12-04 20:40:46 +00:00
c2500c39e6 Merge branch 'main' into move-tests-to-electon 2024-12-04 13:39:51 -05:00
38016e0137 Fix a ton of syntax changes and deflake 2 more tests (pain) 2024-12-03 20:03:54 -05:00
424b409cc1 Bump and release kcl-lib 0.2.27 (#4643)
* bump and release kcl-lib

* update snapshot

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

* empty commit

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-03 17:58:21 -05:00
82a58e69c2 Bump wasm-pack from 0.13.0 to 0.13.1 (#4630) 2024-12-03 16:27:16 -06:00
max
776b420031 Rename addFillet files to addEdgeTreatment (#4644)
* rename

* update references
2024-12-04 08:30:02 +11:00
1087d4223b Bump happy-dom from 15.10.2 to 15.11.7 (#4625)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 15.10.2 to 15.11.7.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v15.10.2...v15.11.7)

---
updated-dependencies:
- dependency-name: happy-dom
  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-12-03 15:40:35 -05:00
089d6df889 Update fn syntax in module docs (#4641) 2024-12-03 15:14:32 -05:00
efb067af58 Update fn syntax in module docs (#4641) 2024-12-03 19:47:21 +00:00
2aa27eab01 Bump happy-dom from 15.10.2 to 15.11.7 (#4625)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 15.10.2 to 15.11.7.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v15.10.2...v15.11.7)

---
updated-dependencies:
- dependency-name: happy-dom
  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-12-03 19:40:36 +00:00
9c47ac5b57 Update fn syntax in KCL Types doc (#4640)
* update fn syntax

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

* Trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-12-03 18:17:02 +00:00
b263ef049a Pass segment-overlays (minor ache) and ability to switch to web if needed :) 2024-12-03 13:13:01 -05:00
5ae1aecd74 Reduce Python API surface area to what is necessary for kcl.py (#4637)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-03 17:34:58 +13:00
68ae7e98f9 Refactor SourceRange and ModuleId to make them better encapsualated (#4636)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-12-03 16:39:51 +13:00
56771d561a Bump rustls from 0.23.13 to 0.23.19 and rustls-pki-types (#4632) 2024-12-03 15:49:15 +13:00
f09411817c KCL AST: Call functions with keyword arguments (#4599)
Call expressions only, haven't done function expressions yet.

Part of https://github.com/KittyCAD/modeling-app/issues/4600
2024-12-02 21:23:18 +00:00
max
bed7ae3b8b Refactor addFillet into addEdgeTreatment Function Supporting Chamfers (#4593)
* refactor code mod and tests

* tsc

* make lint happy

* remove dumby data

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

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-12-02 21:43:59 +01:00
c43510732c KCL docs: Remove FunctionExpression (#4633)
KCL functions are a weird edge case, and the `FunctionExpression` field should not be included in its public API. That field is only there for implementation details, it shouldn't be exposed to users.

What's worse is that `FunctionExpression` includes a `Program` so every single AST node wound up being included in our docs.
2024-12-02 14:36:49 -06:00
51f0b669a4 fix: only count something as a directory if it has children (#4595)
* fix: only count something as a directory if it has children

* fix: playwright tests

* fix: return 0 if you cant find the projectfolder

* fix: remove folder count from e2e tests since it is unused currently

---------

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
2024-12-02 15:16:43 -05:00
3cbedcd3e7 Bump clap from 4.5.20 to 4.5.21 in /src/wasm-lib (#4623)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.20 to 4.5.21.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.20...clap_complete-v4.5.21)

---
updated-dependencies:
- dependency-name: clap
  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-12-02 09:28:57 -08:00
5d2fa43150 Bump dawidd6/action-download-artifact from 6 to 7 (#4621)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 6 to 7.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](https://github.com/dawidd6/action-download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  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-12-02 09:27:57 -08:00
ec49b0752e Remove auto creation of the draft release (#4588)
Fixes #4586
2024-12-02 10:30:24 -05:00
3b171fb881 Update nightly and release icons from Figma (#4597)
* Update nightly and windows icons

* Remove icon.ico, keep only icon.png, see https://www.electron.build/icons.html#windows-nsis

* Revert "Remove icon.ico, keep only icon.png, see https://www.electron.build/icons.html#windows-nsis"

This reverts commit b97f81b07d.

* Update windows icons

* Reset windows ico

* Test ico no margin

* Converted with freeconvert

* Use convertico.com for conversion
2024-12-02 10:24:14 -05:00
1bde0e7333 Pass text-to-cad tests 2024-11-29 18:35:10 -05:00
2301cc153a Pass app header tests 2024-11-29 17:44:41 -05:00
545332ee27 Pass samples loading tests 2024-11-29 17:34:16 -05:00
725e207bd1 Pass perspective-toggle, interesting note 2024-11-29 17:21:55 -05:00
75819f880e Pass another painful spec suite: testing-settings 2024-11-29 17:01:40 -05:00
3c1c1303e1 Merge main (tests changed x_x) and pass all constraints.spec tests (pain) 2024-11-29 14:12:19 -05:00
60e62d435e Merge branch 'main' into move-tests-to-electon 2024-11-29 11:47:16 -05:00
78616f4074 Extreme time eaten by gizmo test fixes. All passing now. 2024-11-28 18:11:28 -05:00
0548409da0 Bump to Rust 1.83 (#4604) 2024-11-28 18:31:11 +00:00
566183ffa7 Pass camera-movement.spec tests 2024-11-28 12:54:09 -05:00
3e435bebf4 Pass network and connection tests 2024-11-28 12:44:08 -05:00
faae28b025 Pass regresion-tests.spec tests 2024-11-28 12:14:27 -05:00
2d91549a02 Pass projects, fought hard with filechooser 2024-11-28 11:49:12 -05:00
dd052b35fd KCL: Remove unnecessary 'optional: bool' field on CallExpression (#4584)
It was put there in the original KCL JS-to-Rust rewrite, and I don't think it's used at all.
2024-11-27 18:01:42 -06:00
46be4e7eef Log simple performance metrics (#4596)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-11-28 11:27:17 +13:00
46874f4d0a Pass machine.spec tests 2024-11-27 15:59:45 -05:00
a132b35f99 Painfully fix hardcoded coordinates in point-click.spec 2024-11-27 15:56:01 -05:00
6e04760e1c Corrected a fixme in file-tree.spec! 2024-11-27 14:02:01 -05:00
082b1cad74 Pass onboarding tests 2024-11-27 11:22:36 -05:00
412d1b7a99 Update KCL Types doc (#4591)
* use `=` instead of `:`, fix formatting

* remove `%` from `segEnd` and `segLen` calls
2024-11-27 11:14:59 -05:00
cfdd22af74 Add ability to immediately enter sketch mode by double-clicking an existing sketch (#4573)
* Implement the functionality

* Another fmt

* Fix handler to not rely on modelingMachine's context,
because that creates an implicit race

* Write an E2E test

* Fix tsc and fmt

* Use artifactGraph helpers for more concise code

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

* Fix up imports and whatnot from commit 2bfc5f5c

* Make early return more clear with curly braces

* Whoops should have linted

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-11-27 10:08:23 -05:00
68a11e7aa5 Remove the lexer from KCL's API (#4589)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-11-27 03:30:28 +00:00
3139e18dc7 Make = and => optional in function declarations (#4577)
* Make `=` and `=>` optional in function declarations

And requires `:` for return types

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Tests

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Format types in function decls

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Require  in anon function decls

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2024-11-27 15:46:58 +13:00
84ddae384e Pass file tree tests 2024-11-26 10:53:59 -05:00
7566954b13 Pass editor tests 2024-11-26 10:30:59 -05:00
4b8c37c2c8 Pass desktop export tests 2024-11-25 14:17:46 -05:00
6d69b560be Pass debug pane tests 2024-11-25 12:51:49 -05:00
aa81d616cc Completely fix code mirror text navigating for tests 2024-11-25 12:50:09 -05:00
7ee5a1c26b fmt 2024-11-22 16:09:09 -05:00
0763d1d11d Pass command bar tests 2024-11-22 16:03:18 -05:00
0f52d7b21f Pass can-sketch-on-all-planes... with ease 2024-11-22 14:45:08 -05:00
25d252787c Pass various.spec.ts, by far the hardest one 2024-11-22 14:34:32 -05:00
1bd5dc20ef Go back to fix up sketch-tests test 2024-11-21 12:00:32 -05:00
cd6b9c2166 Pass testing-selections.spec.ts 2024-11-21 10:07:47 -05:00
f79a59d415 Got testing-segment-overlays passing 2024-11-20 16:45:57 -05:00
eda97a3058 Try out 4 workers 2024-11-20 16:45:57 -05:00
5cbb9d0697 Get sketch-tests.spec.ts passing in electron 2024-11-20 16:45:57 -05:00
be3f9b7b64 Add shebang to script and add macos-14-large as a target 2024-11-20 16:45:57 -05:00
6179c66ae9 Pass the correct param to playwright-electron.sh 2024-11-20 16:45:56 -05:00
117957d7c2 Move all tests over to electron 2024-11-20 16:45:56 -05:00
536 changed files with 117440 additions and 141099 deletions

View File

@ -1,59 +0,0 @@
# bash strict mode
set -euo pipefail
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
if [[ "$3" == ubuntu-latest* ]]; then
yarn test:playwright:browser:chrome:ubuntu -- --shard=$1/$2 || true
elif [[ "$3" == windows-latest* ]]; then
yarn test:playwright:browser:chrome:windows -- --shard=$1/$2 || true
else
echo "Do not run playwright. Unable to detect os runtime."
exit 1
fi
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
retry=1
max_retrys=4
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
if [[ "$3" == ubuntu-latest* ]]; then
yarn test:playwright:browser:chrome:ubuntu -- --last-failed || true
elif [[ "$3" == windows-latest* ]]; then
yarn test:playwright:browser:chrome:windows -- --last-failed || true
else
echo "Do not run playwright. Unable to detect os runtime."
exit 1
fi
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
done
echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
# if it still fails after 3 retrys, then fail the job
exit 1
fi
fi
exit 0

View File

@ -1,15 +1,17 @@
#!/bin/bash
# bash strict mode # bash strict mode
set -euo pipefail set -euo pipefail
if [[ ! -f "test-results/.last-run.json" ]]; then if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally # if no last run artifact, than run plawright normally
echo "run playwright normally" echo "run playwright normally"
if [[ "$1" == ubuntu-latest* ]]; then if [[ "$3" == ubuntu-latest* ]]; then
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu || true xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --shard=$1/$2 || true
elif [[ "$1" == windows-latest* ]]; then elif [[ "$3" == windows-latest* ]]; then
yarn test:playwright:electron:windows || true yarn test:playwright:electron:windows -- --shard=$1/$2 || true
elif [[ "$1" == macos-14* ]]; then elif [[ "$3" == macos-14* ]]; then
yarn test:playwright:electron:macos || true yarn test:playwright:electron:macos -- --shard=$1/$2 || true
else else
echo "Do not run playwright. Unable to detect os runtime." echo "Do not run playwright. Unable to detect os runtime."
exit 1 exit 1
@ -28,11 +30,11 @@ while [[ $retry -le $max_retrys ]]; do
if [[ $failed_tests -gt 0 ]]; then if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry" echo "run playwright with last failed tests and retry $retry"
if [[ "$1" == ubuntu-latest* ]]; then if [[ "$3" == ubuntu-latest* ]]; then
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --last-failed || true xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --last-failed || true
elif [[ "$1" == windows-latest* ]]; then elif [[ "$3" == windows-latest* ]]; then
yarn test:playwright:electron:windows -- --last-failed || true yarn test:playwright:electron:windows -- --last-failed || true
elif [[ "$1" == macos-14* ]]; then elif [[ "$3" == macos-14* ]]; then
yarn test:playwright:electron:macos -- --last-failed || true yarn test:playwright:electron:macos -- --last-failed || true
else else
echo "Do not run playwright. Unable to detect os runtime." echo "Do not run playwright. Unable to detect os runtime."

View File

@ -362,6 +362,17 @@ jobs:
- name: List artifacts - name: List artifacts
run: "ls -R out" run: "ls -R out"
- name: Set more complete nightly release notes
if: ${{ env.IS_NIGHTLY == 'true' }}
run: |
# Note: prefered going this way instead of a full clone in the checkout step,
# see https://github.com/actions/checkout/issues/1471
git fetch --prune --unshallow --tags
export TAG="nightly-${VERSION}"
export PREVIOUS_TAG=$(git describe --tags --match="nightly-v[0-9]*" --abbrev=0)
export NOTES=$(./scripts/get-nightly-changelog.sh)
yarn files:set-notes
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_NIGHTLY == 'true' }}
uses: 'google-github-actions/auth@v2.1.7' uses: 'google-github-actions/auth@v2.1.7'
@ -383,12 +394,13 @@ jobs:
parent: false parent: false
destination: 'dl.kittycad.io/releases/modeling-app/nightly' destination: 'dl.kittycad.io/releases/modeling-app/nightly'
- name: Create draft release - name: Tag nightly commit
uses: softprops/action-gh-release@v2 if: ${{ env.IS_NIGHTLY == 'true' }}
if: ${{ env.IS_RELEASE == 'true' }} uses: actions/github-script@v7
with: with:
name: ${{ env.VERSION }} script: |
tag_name: ${{ env.VERSION }} const { VERSION } = process.env
draft: true const { owner, repo } = context.repo
generate_release_notes: true const { sha } = context
files: 'out/Zoo*' const ref = `refs/tags/nightly-${VERSION}`
github.rest.git.createRef({ owner, repo, sha, ref })

View File

@ -1,7 +1,7 @@
name: E2E Tests name: E2E Tests
on: on:
push: push:
branches: [ main ] branches: [ main, pierremtb/move-tests-to-electron ]
pull_request: pull_request:
branches: [ main ] branches: [ main ]
@ -33,13 +33,13 @@ jobs:
rust: rust:
- 'src/wasm-lib/**' - 'src/wasm-lib/**'
browser: electron:
timeout-minutes: ${{ matrix.os == 'macos-14' && 60 || 50 }} timeout-minutes: ${{ matrix.os == 'macos-14' && 60 || 50 }}
name: playwright:browser:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest-8-cores, windows-latest-8-cores] os: [ubuntu-latest-8-cores, windows-latest-8-cores, macos-14-large]
shardIndex: [1, 2, 3, 4] shardIndex: [1, 2, 3, 4]
shardTotal: [4] shardTotal: [4]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -68,7 +68,7 @@ jobs:
- name: Download Wasm Cache - name: Download Wasm Cache
id: download-wasm id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false' if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v6 uses: dawidd6/action-download-artifact@v7
continue-on-error: true continue-on-error: true
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
@ -123,13 +123,13 @@ jobs:
if: steps.download-wasm.outcome == 'failure' if: steps.download-wasm.outcome == 'failure'
shell: bash shell: bash
run: yarn build:wasm run: yarn build:wasm
- name: build web - name: build electron
run: yarn build:local
shell: bash shell: bash
run: yarn tron:package
- name: Run ubuntu/chrome snapshots - name: Run ubuntu/chrome snapshots
shell: bash shell: bash
run: | run: |
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env: env:
CI: true CI: true
NODE_ENV: development NODE_ENV: development
@ -186,12 +186,12 @@ jobs:
with: with:
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }} name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/ path: test-results/
- name: Run playwright/chrome flow (with retries) - name: Run playwright/electron flow (with retries)
id: retry id: retry
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
shell: bash shell: bash
run: | run: |
.github/ci-cd-scripts/playwright-browser-chrome.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{matrix.os}} .github/ci-cd-scripts/playwright-electron.sh ${{matrix.shardIndex}} ${{matrix.shardTotal}} ${{matrix.os}}
env: env:
CI: true CI: true
FAIL_ON_CONSOLE_ERRORS: true FAIL_ON_CONSOLE_ERRORS: true
@ -199,11 +199,6 @@ jobs:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true VITE_KC_SKIP_AUTH: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: send to axiom
if: always()
shell: bash
run: |
node playwrightProcess.mjs | tee /tmp/github-actions.log
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
@ -221,136 +216,3 @@ jobs:
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
electron:
name: playwright:electron:${{matrix.os}}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest-8-cores, windows-latest-8-cores, macos-14-large]
timeout-minutes: 60
runs-on: ${{ matrix.os }}
needs: check-rust-changes
steps:
- name: Tune GitHub-hosted runner network
uses: smorimoto/tune-github-hosted-runner-network@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- uses: KittyCAD/action-install-cli@main
- name: Install dependencies
shell: bash
run: yarn
- name: Cache Playwright Browsers
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright/
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
- name: Install Playwright Browsers
shell: bash
run: yarn playwright install chromium --with-deps
- name: Download Wasm Cache
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v6
continue-on-error: true
with:
github_token: ${{secrets.GITHUB_TOKEN}}
name: wasm-bundle
workflow: build-and-store-wasm.yml
branch: main
path: src/wasm-lib/pkg
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
shell: bash
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: OR Cache Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: install good sed
if: ${{ startsWith(matrix.os, 'macos') }}
shell: bash
run: |
brew install gnu-sed
echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
- name: Install vector
if: ${{ startsWith(matrix.os, 'ubuntu') }}
shell: bash
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
shell: bash
run: yarn build:wasm
- name: OR Build Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
shell: bash
run: yarn build:wasm
- name: build electron
shell: bash
run: yarn tron:package
- uses: actions/download-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true
with:
name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
path: test-results/
- name: Run electron tests (with retries)
id: retry
if: ${{ !cancelled() && (success() || failure()) }}
shell: bash
run: |
.github/ci-cd-scripts/playwright-electron.sh ${{ matrix.os }}
env:
CI: true
FAIL_ON_CONSOLE_ERRORS: true
NODE_ENV: development
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true
IS_UBUNTU: ${{ startsWith(matrix.os, 'ubuntu') && 'true' || 'false' }}
#DEBUG: 'pw:browser*'
- name: send to axiom
if: ${{ !cancelled() && (success() || failure()) && !startsWith(matrix.os, 'windows') }}
shell: bash
run: |
node playwrightProcess.mjs | tee /tmp/github-actions.log
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
with:
name: test-results-electron-${{ matrix.os }}-${{ github.sha }}
path: test-results/
include-hidden-files: true
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
with:
name: playwright-report-electron-${{ matrix.os }}-${{ github.sha }}
path: playwright-report/
include-hidden-files: true
retention-days: 30
overwrite: true

View File

@ -132,6 +132,12 @@ jobs:
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async
- name: Upload release files to Github
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2
with:
files: 'out/Zoo*'
announce_release: announce_release:
needs: [publish-apps-release] needs: [publish-apps-release]

1
.gitignore vendored
View File

@ -61,6 +61,7 @@ Mac_App_Distribution.provisionprofile
*.tsbuildinfo *.tsbuildinfo
src/wasm-lib/pkg src/wasm-lib/pkg
.eslintcache
venv venv
.vite/ .vite/

View File

@ -99,7 +99,7 @@ yarn tron:start
This will start the application and hot-reload on changes. This will start the application and hot-reload on changes.
Devtools can be opened with the usual Cmd/Ctrl-Shift-I. Devtools can be opened with the usual Cmd-Opt-I (Mac) or Ctrl-Shift-I (Linux and Windows).
To build, run `yarn tron:package`. To build, run `yarn tron:package`.
@ -136,7 +136,7 @@ https://github.com/KittyCAD/modeling-app/issues/new
#### 2. Push a new tag #### 2. Push a new tag
Create a new tag and push it to the repo (eg. `v0.28.0` for `$VERSION`) Create a new tag and push it to the repo. The `semantic-release.sh` script will automatically bump the minor part, which we use the most. For instance going from `v0.27.0` to `v0.28.0`.
``` ```
VERSION=$(./scripts/semantic-release.sh) VERSION=$(./scripts/semantic-release.sh)
@ -146,16 +146,14 @@ git push origin --tags
This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files as well as updater-test artifacts. This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files as well as updater-test artifacts.
Once the workflow succeeds, a draft release will be created at https://github.com/KittyCAD/modeling-app/releases. The workflow should be listed right away [in this list](https://github.com/KittyCAD/modeling-app/actions/workflows/build-apps.yml?query=event%3Apush)).
#### 3. Manually test artifacts from the Cut Release PR #### 3. Manually test artifacts
##### Release builds ##### Release builds
The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.). The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.).
Alternatively, the draft release will also include these builds.
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue. Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue.
##### Updater-test builds ##### Updater-test builds
@ -178,9 +176,11 @@ If the prompt doesn't show up, start the app in command line to grab the electro
#### 4. Publish the release #### 4. Publish the release
Head over to https://github.com/KittyCAD/modeling-app/releases, paste in the changelog discussed in the issue, and publish the draft release created by the `build-apps` workflow from step 2. Head over to https://github.com/KittyCAD/modeling-app/releases/new, pick the newly created tag and type it in the _Release title_ field as well.
A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter. On success, the files will be uploaded to the public bucket and the announcement on Discord will be sent. Hit _Generate release notes_ as a starting point to discuss the changelog in the issue. Once done, make sure _Set as the latest release_ is checked, and hit _Publish release_.
A new `publish-apps-release` will kick in and you should be able to find it [here](https://github.com/KittyCAD/modeling-app/actions?query=event%3Arelease). On success, the files will be uploaded to the public bucket as well as to the GitHub release, and the announcement on Discord will be sent.
#### 5. Close the issue #### 5. Close the issue
@ -388,23 +388,6 @@ yarn test:unit:local
#### E2E Tests #### E2E Tests
**Playwright Browser**
These E2E tests run in a browser (without electron).
There are tests that are skipped if they are ran in a windows OS or Linux OS. We can use playwright tags to implement test skipping.
Breaking down the command `yarn test:playwright:browser:chrome:windows`
- The application is `playwright`
- The runtime is a `browser`
- The specific `browser` is `chrome`
- The test should run in a `windows` environment. It will skip tests that are broken or flaky in the windows OS.
```
yarn test:playwright:browser:chrome
yarn test:playwright:browser:chrome:windows
yarn test:playwright:browser:chrome:ubuntu
```
**Playwright Electron** **Playwright Electron**
These E2E tests run in electron. There are tests that are skipped if they are ran in a windows, linux, or macos environment. We can use playwright tags to implement test skipping. These E2E tests run in electron. There are tests that are skipped if they are ran in a windows, linux, or macos environment. We can use playwright tags to implement test skipping.
@ -450,3 +433,9 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
## KCL ## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl). For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).
### Logging
To display logging (to the terminal or console) set `ZOO_LOG=1`. This will log some warnings and simple performance metrics. To view these in test runs, use `-- --nocapture`.
To enable memory metrics, build with `--features dhat-heap`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -58,7 +58,7 @@ mountingPlate = extrude(thickness, mountingPlateSketch)
```js ```js
// Sketch on the face of a chamfer. // Sketch on the face of a chamfer.
fn cube = (pos, scale) => { fn cube(pos, scale) {
sg = startSketchOn('XY') sg = startSketchOn('XY')
|> startProfileAt(pos, %) |> startProfileAt(pos, %)
|> line([0, scale], %) |> line([0, scale], %)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -37,7 +37,7 @@ assertEqual(n, 3, 0.0001, "5/2 = 2.5, rounded up makes 3")
startSketchOn('XZ') startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 2 }, %) |> circle({ center = [0, 0], radius = 2 }, %)
|> extrude(5, %) |> extrude(5, %)
|> patternTransform(n, (id) => { |> patternTransform(n, fn(id) {
return { translate = [4 * id, 0, 0] } return { translate = [4 * id, 0, 0] }
}, %) }, %)
``` ```

View File

@ -29,7 +29,7 @@ map(array: [KclValue], map_fn: FunctionParam) -> [KclValue]
```js ```js
r = 10 // radius r = 10 // radius
fn drawCircle = (id) => { fn drawCircle(id) {
return startSketchOn("XY") return startSketchOn("XY")
|> circle({ center = [id * 2 * r, 0], radius = r }, %) |> circle({ center = [id * 2 * r, 0], radius = r }, %)
} }
@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
```js ```js
r = 10 // radius r = 10 // radius
// Call `map`, using an anonymous function instead of a named one. // Call `map`, using an anonymous function instead of a named one.
circles = map([1..3], (id) => { circles = map([1..3], (id) {
return startSketchOn("XY") return startSketchOn("XY")
|> circle({ center = [id * 2 * r, 0], radius = r }, %) |> circle({ center = [id * 2 * r, 0], radius = r }, %)
}) })

View File

@ -12,7 +12,7 @@ to other modules.
``` ```
// util.kcl // util.kcl
export fn increment = (x) => { export fn increment(x) {
return x + 1 return x + 1
} }
``` ```
@ -37,11 +37,11 @@ Multiple functions can be exported in a file.
``` ```
// util.kcl // util.kcl
export fn increment = (x) => { export fn increment(x) {
return x + 1 return x + 1
} }
export fn decrement = (x) => { export fn decrement(x) {
return x - 1 return x - 1
} }
``` ```

File diff suppressed because one or more lines are too long

View File

@ -30,7 +30,7 @@ patternTransform2d(total_instances: u32, transform_function: FunctionParam, soli
```js ```js
// Each instance will be shifted along the X axis. // Each instance will be shifted along the X axis.
fn transform = (id) => { fn transform(id) {
return { translate = [4 * id, 0] } return { translate = [4 * id, 0] }
} }

View File

@ -30,14 +30,14 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
```js ```js
// This function adds two numbers. // This function adds two numbers.
fn add = (a, b) => { fn add(a, b) {
return a + b return a + b
} }
// This function adds an array of numbers. // This function adds an array of numbers.
// It uses the `reduce` function, to call the `add` function on every // It uses the `reduce` function, to call the `add` function on every
// element of the `arr` parameter. The starting value is 0. // element of the `arr` parameter. The starting value is 0.
fn sum = (arr) => { fn sum(arr) {
return reduce(arr, 0, add) return reduce(arr, 0, add)
} }
@ -61,7 +61,7 @@ assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
// an anonymous `add` function as its parameter, instead of declaring a // an anonymous `add` function as its parameter, instead of declaring a
// named function outside. // named function outside.
arr = [1, 2, 3] arr = [1, 2, 3]
sum = reduce(arr, 0, (i, result_so_far) => { sum = reduce(arr, 0, (i, result_so_far) {
return i + result_so_far return i + result_so_far
}) })
@ -74,7 +74,7 @@ assertEqual(sum, 6, 0.00001, "1 + 2 + 3 summed is 6")
```js ```js
// Declare a function that sketches a decagon. // Declare a function that sketches a decagon.
fn decagon = (radius) => { fn decagon(radius) {
// Each side of the decagon is turned this many degrees from the previous angle. // Each side of the decagon is turned this many degrees from the previous angle.
stepAngle = 1 / 10 * tau() stepAngle = 1 / 10 * tau()
@ -84,7 +84,7 @@ fn decagon = (radius) => {
// Use a `reduce` to draw the remaining decagon sides. // Use a `reduce` to draw the remaining decagon sides.
// For each number in the array 1..10, run the given function, // For each number in the array 1..10, run the given function,
// which takes a partially-sketched decagon and adds one more edge to it. // which takes a partially-sketched decagon and adds one more edge to it.
fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) => { fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {
// Draw one edge of the decagon. // Draw one edge of the decagon.
x = cos(stepAngle * i) * radius x = cos(stepAngle * i) * radius
y = sin(stepAngle * i) * radius y = sin(stepAngle * i) * radius

File diff suppressed because one or more lines are too long

View File

@ -36,7 +36,7 @@ cube = startSketchAt([0, 0])
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
fn cylinder = (radius, tag) => { fn cylinder(radius, tag) {
return startSketchAt([0, 0]) return startSketchAt([0, 0])
|> circle({ |> circle({
radius = radius, radius = radius,

View File

@ -36,7 +36,7 @@ cube = startSketchAt([0, 0])
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
fn cylinder = (radius, tag) => { fn cylinder(radius, tag) {
return startSketchAt([0, 0]) return startSketchAt([0, 0])
|> circle({ |> circle({
radius = radius, radius = radius,

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,7 @@ If you want to get a value from an array you can use the index like so:
An object is defined with `{}` braces. Here is an example object: An object is defined with `{}` braces. Here is an example object:
``` ```
myObj = {a: 0, b: "thing"} myObj = { a = 0, b = "thing" }
``` ```
We support two different ways of getting properties from objects, you can call We support two different ways of getting properties from objects, you can call
@ -54,7 +54,7 @@ We also have support for defining your own functions. Functions can take in any
type of argument. Below is an example of the syntax: type of argument. Below is an example of the syntax:
``` ```
fn myFn = (x) => { fn myFn(x) {
return x return x
} }
``` ```
@ -90,12 +90,12 @@ startSketchOn('XZ')
|> startProfileAt(origin, %) |> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001) |> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001, %) - 90, segAng(rectangleSegmentA001) - 90,
196.99 196.99
], %, $rectangleSegmentB001) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001, %), segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001, %) -segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
@ -118,17 +118,17 @@ use the tag `rectangleSegmentA001` in any function or expression in the file.
However if the code was written like this: However if the code was written like this:
``` ```
fn rect = (origin) => { fn rect(origin) {
return startSketchOn('XZ') return startSketchOn('XZ')
|> startProfileAt(origin, %) |> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001) |> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001, %) - 90, segAng(rectangleSegmentA001) - 90,
196.99 196.99
], %, $rectangleSegmentB001) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001, %), segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001, %) -segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
@ -146,17 +146,17 @@ Tags are accessible through the sketch group they are declared in.
For example the following code works. For example the following code works.
``` ```
fn rect = (origin) => { fn rect(origin) {
return startSketchOn('XZ') return startSketchOn('XZ')
|> startProfileAt(origin, %) |> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001) |> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001, %) - 90, segAng(rectangleSegmentA001) - 90,
196.99 196.99
], %, $rectangleSegmentB001) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001, %), segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001, %) -segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
@ -167,7 +167,10 @@ myRect = rect([20, 0])
myRect myRect
|> extrude(10, %) |> extrude(10, %)
|> fillet({radius: 0.5, tags: [myRect.tags.rectangleSegmentA001]}, %) |> fillet({
radius = 0.5,
tags = [myRect.tags.rectangleSegmentA001]
}, %)
``` ```
See how we use the tag `rectangleSegmentA001` in the `fillet` function outside See how we use the tag `rectangleSegmentA001` in the `fillet` function outside

View File

@ -1,161 +0,0 @@
---
title: "BinaryOperator"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Add two numbers.
**enum:** `+`
----
Subtract two numbers.
**enum:** `-`
----
Multiply two numbers.
**enum:** `*`
----
Divide two numbers.
**enum:** `/`
----
Modulo two numbers.
**enum:** `%`
----
Raise a number to a power.
**enum:** `^`
----
Are two numbers equal?
**enum:** `==`
----
Are two numbers not equal?
**enum:** `!=`
----
Is left greater than right
**enum:** `>`
----
Is left greater than or equal to right
**enum:** `>=`
----
Is left less than right
**enum:** `<`
----
Is left less than or equal to right
**enum:** `<=`
----

View File

@ -1,161 +0,0 @@
---
title: "BinaryPart"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `BinaryExpression`| | No |
| `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| | No |
| `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CallExpression`| | No |
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `optional` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UnaryExpression`| | No |
| `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| | No |
| `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `IfExpression`| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| | No |
| `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`| | No |
| `final_else` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -1,97 +0,0 @@
---
title: "BodyItem"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ImportStatement`| | No |
| `items` |`[` [`ImportItem`](/docs/kcl/types/ImportItem) `]`| | No |
| `path` |`string`| | No |
| `raw_path` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ExpressionStatement`| | No |
| `expression` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `VariableDeclaration`| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ReturnStatement`| | No |
| `argument` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -1,41 +0,0 @@
---
title: "CommentStyle"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Like // foo
**enum:** `line`
----
Like /* foo */
**enum:** `block`
----

View File

@ -1,24 +0,0 @@
---
title: "ElseIf"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `cond` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,16 +0,0 @@
---
title: "EnvironmentRef"
excerpt: "An index pointing to an environment."
layout: manual
---
An index pointing to an environment.
**Type:** `integer` (`uint`)

View File

@ -1,318 +0,0 @@
---
title: "Expr"
excerpt: "An expression can be evaluated to yield a single KCL value."
layout: manual
---
An expression can be evaluated to yield a single KCL value.
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| An expression can be evaluated to yield a single KCL value. | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`TagDeclarator`](/docs/kcl/types#tag-declaration)| | No |
| `value` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `BinaryExpression`| | No |
| `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| An expression can be evaluated to yield a single KCL value. | No |
| `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`FunctionExpression`](/docs/kcl/types/FunctionExpression)| | No |
| `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`| | No |
| `body` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CallExpression`| | No |
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| An expression can be evaluated to yield a single KCL value. | No |
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `optional` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `PipeExpression`| | No |
| `body` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `PipeSubstitution`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayExpression`| | No |
| `elements` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayRangeExpression`| | No |
| `startElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endInclusive` |`boolean`| Is the `end_element` included in the range? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ObjectExpression`| | No |
| `properties` |`[` [`ObjectProperty`](/docs/kcl/types/ObjectProperty) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| An expression can be evaluated to yield a single KCL value. | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| An expression can be evaluated to yield a single KCL value. | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UnaryExpression`| | No |
| `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| An expression can be evaluated to yield a single KCL value. | No |
| `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `IfExpression`| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`| | No |
| `final_else` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application).
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `None`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -1,24 +0,0 @@
---
title: "FunctionExpression"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`| | No |
| `body` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,23 +0,0 @@
---
title: "Identifier"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,24 +0,0 @@
---
title: "ImportItem"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `name` |[`Identifier`](/docs/kcl/types/Identifier)| Name of the item to import. | No |
| `alias` |[`Identifier`](/docs/kcl/types/Identifier)| Rename the item using an identifier after "as". | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,16 +0,0 @@
---
title: "ItemVisibility"
excerpt: ""
layout: manual
---
**enum:** `default`, `export`

View File

@ -317,7 +317,6 @@ Data for an imported geometry.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Function`| | No | | `type` |enum: `Function`| | No |
| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| Any KCL value. | No |
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No | | `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -1,56 +0,0 @@
---
title: "LiteralIdentifier"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -1,47 +0,0 @@
---
title: "LiteralValue"
excerpt: ""
layout: manual
---
**This schema accepts any of the following:**
**Type:** `number` (`double`)
----
**Type:** `string`
----
**Type:** `boolean`
----

View File

@ -1,57 +0,0 @@
---
title: "MemberObject"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -1,22 +0,0 @@
---
title: "NonCodeMeta"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `nonCodeNodes` |`object`| | No |
| `startNodes` |`[` [`NonCodeNode`](/docs/kcl/types/NonCodeNode) `]`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -1,23 +0,0 @@
---
title: "NonCodeNode"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `value` |[`NonCodeValue`](/docs/kcl/types/NonCodeValue)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,86 +0,0 @@
---
title: "NonCodeValue"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `inlineComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
A block comment. An example of this is the following: ```python,no_run /* This is a block comment */ 1 + 1 ``` Now this is important. The block comment is attached to the next line. This is always the case. Also the block comment doesn't have a new line above it. If it did it would be a `NewLineBlockComment`.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `blockComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
A block comment that has a new line above it. The user explicitly added a new line above the block comment.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `newLineBlockComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `newLine`| | No |
----

View File

@ -1,24 +0,0 @@
---
title: "ObjectProperty"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `key` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
| `value` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,23 +0,0 @@
---
title: "Parameter"
excerpt: "Parameter of a KCL function."
layout: manual
---
Parameter of a KCL function.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `identifier` |[`Identifier`](/docs/kcl/types/Identifier)| The parameter's label or name. | No |
| `optional` |`boolean`| Is the parameter optional? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -1,26 +0,0 @@
---
title: "Program"
excerpt: "A KCL program top level, or function body."
layout: manual
---
A KCL program top level, or function body.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `body` |`[` [`BodyItem`](/docs/kcl/types/BodyItem) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| A KCL program top level, or function body. | No |
| `shebang` |[`Shebang`](/docs/kcl/types/Shebang)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,23 +0,0 @@
---
title: "Shebang"
excerpt: "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```"
layout: manual
---
A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `content` |`string`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,15 +0,0 @@
---
title: "Uint"
excerpt: ""
layout: manual
---
**Type:** `integer` (`uint32`)

View File

@ -1,41 +0,0 @@
---
title: "UnaryOperator"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Negate a number.
**enum:** `-`
----
Negate a boolean.
**enum:** `!`
----

View File

@ -1,24 +0,0 @@
---
title: "VariableDeclarator"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `id` |[`Identifier`](/docs/kcl/types/Identifier)| The identifier of the variable. | No |
| `init` |[`Expr`](/docs/kcl/types/Expr)| The value of the variable. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,41 +0,0 @@
---
title: "VariableKind"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Declare a named constant.
**enum:** `const`
----
Declare a function.
**enum:** `fn`
----

View File

@ -1,22 +1,11 @@
import { test, expect } from '@playwright/test' import { test, expect } from './zoo-test'
import { setupElectron, tearDown } from './test-utils'
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Electron app header tests', () => { test.describe('Electron app header tests', () => {
test( test(
'Open Command Palette button has correct shortcut', 'Open Command Palette button has correct shortcut',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await page.setBodyDimensions({ width: 1200, height: 500 })
testInfo,
folderSetupFn: async () => {},
})
await page.setViewportSize({ width: 1200, height: 500 })
// No space before the shortcut since it checks textContent. // No space before the shortcut since it checks textContent.
let text let text
@ -34,21 +23,14 @@ test.describe('Electron app header tests', () => {
const commandsButton = page.getByRole('button', { name: 'Commands' }) const commandsButton = page.getByRole('button', { name: 'Commands' })
await expect(commandsButton).toBeVisible() await expect(commandsButton).toBeVisible()
await expect(commandsButton).toHaveText(text) await expect(commandsButton).toHaveText(text)
await electronApp.close()
} }
) )
test( test(
'User settings has correct shortcut', 'User settings has correct shortcut',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await page.setBodyDimensions({ width: 1200, height: 500 })
testInfo,
folderSetupFn: async () => {},
})
await page.setViewportSize({ width: 1200, height: 500 })
// Open the user sidebar menu. // Open the user sidebar menu.
await page.getByTestId('user-sidebar-toggle').click() await page.getByTestId('user-sidebar-toggle').click()
@ -59,8 +41,6 @@ test.describe('Electron app header tests', () => {
const userSettingsButton = page.getByTestId('user-settings') const userSettingsButton = page.getByTestId('user-settings')
await expect(userSettingsButton).toBeVisible() await expect(userSettingsButton).toBeVisible()
await expect(userSettingsButton).toHaveText(text) await expect(userSettingsButton).toHaveText(text)
await electronApp.close()
} }
) )
}) })

View File

@ -1,29 +1,26 @@
import { test, expect, Page } from '@playwright/test' import { test, expect, Page } from './zoo-test'
import { import {
getUtils, getUtils,
TEST_COLORS, TEST_COLORS,
setup,
tearDown,
commonPoints, commonPoints,
PERSIST_MODELING_CONTEXT, PERSIST_MODELING_CONTEXT,
} from './test-utils' } from './test-utils'
import { HomePageFixture } from './fixtures/homePageFixture'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.setTimeout(120000) test.setTimeout(120000)
async function doBasicSketch(page: Page, openPanes: string[]) { async function doBasicSketch(
page: Page,
homePage: HomePageFixture,
openPanes: string[]
) {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout()
await u.openDebugPanel() await u.openDebugPanel()
// If we have the code pane open, we should see the code. // If we have the code pane open, we should see the code.
@ -148,13 +145,11 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
} }
test.describe('Basic sketch', () => { test.describe('Basic sketch', () => {
test('code pane open at start', { tag: ['@skipWin'] }, async ({ page }) => { test.fixme('code pane open at start', async ({ page, homePage }) => {
// Skip on windows it is being weird. await doBasicSketch(page, homePage, ['code'])
test.skip(process.platform === 'win32', 'Skip on windows')
await doBasicSketch(page, ['code'])
}) })
test('code pane closed at start', async ({ page }) => { test.fixme('code pane closed at start', async ({ page, homePage }) => {
// Load the app with the code panes // Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => { await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem( localStorage.setItem(
@ -162,6 +157,6 @@ test.describe('Basic sketch', () => {
JSON.stringify({ openPanes: [] }) JSON.stringify({ openPanes: [] })
) )
}, PERSIST_MODELING_CONTEXT) }, PERSIST_MODELING_CONTEXT)
await doBasicSketch(page, []) await doBasicSketch(page, homePage, [])
}) })
}) })

View File

@ -1,27 +1,21 @@
import { test, expect } from '@playwright/test' import { test, expect, Page } from './zoo-test'
import { getUtils, setup, tearDown } from './test-utils' import { HomePageFixture } from './fixtures/homePageFixture'
import { getUtils } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Can create sketches on all planes and their back sides', () => { test.describe('Can create sketches on all planes and their back sides', () => {
const sketchOnPlaneAndBackSideTest = async ( const sketchOnPlaneAndBackSideTest = async (
page: any, page: Page,
homePage: HomePageFixture,
plane: string, plane: string,
clickCoords: { x: number; y: number } clickCoords: { x: number; y: number }
) => { ) => {
const u = await getUtils(page) const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.openDebugPanel() await u.openDebugPanel()
const coord = const coord =
@ -83,32 +77,39 @@ test.describe('Can create sketches on all planes and their back sides', () => {
await u.clearCommandLogs() await u.clearCommandLogs()
await u.removeCurrentCode() await u.removeCurrentCode()
} }
test('XY', async ({ page }) => { test('XY', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest( await sketchOnPlaneAndBackSideTest(
page, page,
homePage,
'XY', 'XY',
{ x: 600, y: 388 } // red plane { x: 600, y: 388 } // red plane
// { x: 600, y: 400 }, // red plane // clicks grid helper and that causes problems, should fix so that these coords work too. // { x: 600, y: 400 }, // red plane // clicks grid helper and that causes problems, should fix so that these coords work too.
) )
}) })
test('YZ', async ({ page }) => { test('YZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, 'YZ', { x: 700, y: 250 }) // green plane await sketchOnPlaneAndBackSideTest(page, homePage, 'YZ', { x: 700, y: 250 }) // green plane
}) })
test('XZ', async ({ page }) => { test('XZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, '-XZ', { x: 700, y: 80 }) // blue plane await sketchOnPlaneAndBackSideTest(page, homePage, '-XZ', { x: 700, y: 80 }) // blue plane
}) })
test('-XY', async ({ page }) => { test('-XY', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, '-XY', { x: 600, y: 118 }) // back of red plane await sketchOnPlaneAndBackSideTest(page, homePage, '-XY', {
x: 600,
y: 118,
}) // back of red plane
}) })
test('-YZ', async ({ page }) => { test('-YZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, '-YZ', { x: 700, y: 219 }) // back of green plane await sketchOnPlaneAndBackSideTest(page, homePage, '-YZ', {
x: 700,
y: 219,
}) // back of green plan
}) })
test('-XZ', async ({ page }) => { test('-XZ', async ({ page, homePage }) => {
await sketchOnPlaneAndBackSideTest(page, 'XZ', { x: 700, y: 427 }) // back of blue plane await sketchOnPlaneAndBackSideTest(page, homePage, 'XZ', { x: 700, y: 427 }) // back of blue plane
}) })
}) })

View File

@ -1,28 +1,15 @@
import { test, expect } from '@playwright/test' import { test, expect } from './zoo-test'
import { import { getUtils, executorInputPath } from './test-utils'
getUtils,
setup,
setupElectron,
tearDown,
executorInputPath,
} from './test-utils'
import { join } from 'path' import { join } from 'path'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates' import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates'
import fsp from 'fs/promises' import fsp from 'fs/promises'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Code pane and errors', () => { test.describe('Code pane and errors', () => {
test('Typing KCL errors induces a badge on the code pane button', async ({ test('Typing KCL errors induces a badge on the code pane button', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -31,18 +18,18 @@ test.describe('Code pane and errors', () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`// Extruded Triangle `// Extruded Triangle
sketch001 = startSketchOn('XZ') sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([10, 0], %) |> line([10, 0], %)
|> line([-5, 10], %) |> line([-5, 10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(5, sketch001)` extrude001 = extrude(5, sketch001)`
) )
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -64,9 +51,9 @@ extrude001 = extrude(5, sketch001)`
test('Opening and closing the code pane will consistently show error diagnostics', async ({ test('Opening and closing the code pane will consistently show error diagnostics', async ({
page, page,
homePage,
editor,
}) => { }) => {
await page.goto('http://localhost:3000')
const u = await getUtils(page) const u = await getUtils(page)
// Load the app with the working starter code // Load the app with the working starter code
@ -74,8 +61,8 @@ extrude001 = extrude(5, sketch001)`
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
}, bracket) }, bracket)
await page.setViewportSize({ width: 1200, height: 900 }) await page.setBodyDimensions({ width: 1200, height: 900 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -91,8 +78,9 @@ extrude001 = extrude(5, sketch001)`
await expect(codePaneButtonHolder).not.toContainText('notification') await expect(codePaneButtonHolder).not.toContainText('notification')
// Delete a character to break the KCL // Delete a character to break the KCL
await u.openKclCodePanel() await editor.openPane()
await page.getByText('thickness, bracketLeg1Sketch)').click() await editor.scrollToText('thickness, bracketLeg1Sketch)')
await page.getByText('extrude(thickness, bracketLeg1Sketch)').click()
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
// Ensure that a badge appears on the button // Ensure that a badge appears on the button
@ -116,7 +104,10 @@ extrude001 = extrude(5, sketch001)`
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// Open the code pane // Open the code pane
await u.openKclCodePanel() await editor.openPane()
// Go to our problematic code again (missing closing paren!)
await editor.scrollToText('extrude(thickness, bracketLeg1Sketch')
// Ensure that a badge appears on the button // Ensure that a badge appears on the button
await expect(codePaneButtonHolder).toContainText('notification') await expect(codePaneButtonHolder).toContainText('notification')
@ -129,18 +120,18 @@ extrude001 = extrude(5, sketch001)`
await expect(page.locator('.cm-tooltip').first()).toBeVisible() await expect(page.locator('.cm-tooltip').first()).toBeVisible()
}) })
test('When error is not in view you can click the badge to scroll to it', async ({ test.fixme('When error is not in view you can click the badge to scroll to it', async ({
page, page,
homePage,
context,
}) => { }) => {
const u = await getUtils(page)
// Load the app with the working starter code // Load the app with the working starter code
await page.addInitScript((code) => { await context.addInitScript((code) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
}, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW) }, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
@ -164,24 +155,24 @@ extrude001 = extrude(5, sketch001)`
await expect( await expect(
page page
.getByText( .getByText(
'sketch profile must lie entirely on one side of the revolution axis' 'Modeling command failed: [ApiError { error_code: InternalEngine, message: "Solid3D revolve failed: sketch profile must lie entirely on one side of the revolution axis" }]'
) )
.first() .first()
).toBeVisible() ).toBeVisible()
}) })
test('When error is not in view WITH LINTS you can click the badge to scroll to it', async ({ test('When error is not in view WITH LINTS you can click the badge to scroll to it', async ({
context,
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page)
// Load the app with the working starter code // Load the app with the working starter code
await page.addInitScript((code) => { await context.addInitScript((code) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
}, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW) }, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
@ -241,11 +232,9 @@ extrude001 = extrude(5, sketch001)`
test( test(
'Opening multiple panes persists when switching projects', 'Opening multiple panes persists when switching projects',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
// Setup multiple projects. // Setup multiple projects.
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate') const routerTemplateDir = join(dir, 'router-template-slate')
const bracketDir = join(dir, 'bracket') const bracketDir = join(dir, 'bracket')
await Promise.all([ await Promise.all([
@ -262,11 +251,10 @@ test(
join(bracketDir, 'main.kcl') join(bracketDir, 'main.kcl')
), ),
]) ])
},
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await test.step('Opening the bracket project should load', async () => { await test.step('Opening the bracket project should load', async () => {
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
@ -309,30 +297,21 @@ test(
await expect(page.locator('#variables-pane')).toBeVisible() await expect(page.locator('#variables-pane')).toBeVisible()
await expect(page.locator('#logs-pane')).toBeVisible() await expect(page.locator('#logs-pane')).toBeVisible()
}) })
await electronApp.close()
} }
) )
test( test(
'external change of file contents are reflected in editor', 'external change of file contents are reflected in editor',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const PROJECT_DIR_NAME = 'lee-was-here' const PROJECT_DIR_NAME = 'lee-was-here'
const { const { dir: projectsDir } = await context.folderSetupFn(async (dir) => {
electronApp,
page,
dir: projectsDir,
} = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const aProjectDir = join(dir, PROJECT_DIR_NAME) const aProjectDir = join(dir, PROJECT_DIR_NAME)
await fsp.mkdir(aProjectDir, { recursive: true }) await fsp.mkdir(aProjectDir, { recursive: true })
},
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await test.step('Open the project', async () => { await test.step('Open the project', async () => {
await expect(page.getByText(PROJECT_DIR_NAME)).toBeVisible() await expect(page.getByText(PROJECT_DIR_NAME)).toBeVisible()
@ -351,7 +330,5 @@ test(
) )
await u.editorTextMatches(content) await u.editorTextMatches(content)
}) })
await electronApp.close()
} }
) )

View File

@ -1,19 +1,12 @@
import { test, expect } from '@playwright/test' import { test, expect } from './zoo-test'
import { getUtils, setup, tearDown } from './test-utils' import { getUtils } from './test-utils'
import { KCL_DEFAULT_LENGTH } from 'lib/constants' import { KCL_DEFAULT_LENGTH } from 'lib/constants'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Command bar tests', () => { test.describe('Command bar tests', () => {
test('Extrude from command bar selects extrude line after', async ({ test('Extrude from command bar selects extrude line after', async ({
page, page,
homePage,
}) => { }) => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -29,9 +22,9 @@ test.describe('Command bar tests', () => {
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -52,7 +45,7 @@ test.describe('Command bar tests', () => {
) )
}) })
test('Fillet from command bar', async ({ page }) => { test('Fillet from command bar', async ({ page, homePage }) => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
@ -63,13 +56,13 @@ test.describe('Command bar tests', () => {
|> line([0, -10], %) |> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(-10, sketch001)` extrude001 = extrude(-10, sketch001)`
) )
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
@ -93,10 +86,10 @@ extrude001 = extrude(-10, sketch001)`
test('Command bar can change a setting, and switch back and forth between arguments', async ({ test('Command bar can change a setting, and switch back and forth between arguments', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 }) await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
const commandBarButton = page.getByRole('button', { name: 'Commands' }) const commandBarButton = page.getByRole('button', { name: 'Commands' })
const cmdSearchBar = page.getByPlaceholder('Search commands') const cmdSearchBar = page.getByPlaceholder('Search commands')
@ -153,7 +146,7 @@ extrude001 = extrude(-10, sketch001)`
// Check that the visibility changed // Check that the visibility changed
await expect(paneSelector).not.toBeVisible() await expect(paneSelector).not.toBeVisible()
commandOptionInput = page.getByPlaceholder('off') commandOptionInput = page.locator('[id="option-input"]')
// Test case for https://github.com/KittyCAD/modeling-app/issues/2882 // Test case for https://github.com/KittyCAD/modeling-app/issues/2882
await commandBarButton.click() await commandBarButton.click()
@ -174,10 +167,10 @@ extrude001 = extrude(-10, sketch001)`
test('Command bar keybinding works from code editor and can change a setting', async ({ test('Command bar keybinding works from code editor and can change a setting', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 }) await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
@ -221,7 +214,7 @@ extrude001 = extrude(-10, sketch001)`
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`) await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
}) })
test('Can extrude from the command bar', async ({ page }) => { test('Can extrude from the command bar', async ({ page, homePage }) => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
@ -237,9 +230,9 @@ extrude001 = extrude(-10, sketch001)`
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// Make sure the stream is up // Make sure the stream is up
await u.openDebugPanel() await u.openDebugPanel()
@ -293,26 +286,19 @@ extrude001 = extrude(-10, sketch001)`
await continueButton.click() await continueButton.click()
await submitButton.click() await submitButton.click()
// Check that the code was updated
await u.waitForCmdReceive('extrude') await u.waitForCmdReceive('extrude')
// Unfortunately this indentation seems to matter for the test
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toContainText(
`distance = sqrt(20) 'extrude001 = extrude(distance001, sketch001)'
distance001 = ${KCL_DEFAULT_LENGTH}
sketch001 = startSketchOn('XZ')
|> startProfileAt([-6.95, 10.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -20.93], %)
|> line([-23.44, 0.52], %)
|> close(%)
extrude001 = extrude(distance001, sketch001)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
) )
}) })
test('Can switch between sketch tools via command bar', async ({ page }) => { test('Can switch between sketch tools via command bar', async ({
const u = await getUtils(page) page,
await page.setViewportSize({ width: 1200, height: 500 }) homePage,
await u.waitForAuthSkipAppStart() }) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
const sketchButton = page.getByRole('button', { name: 'Start Sketch' }) const sketchButton = page.getByRole('button', { name: 'Start Sketch' })
const cmdBarButton = page.getByRole('button', { name: 'Commands' }) const cmdBarButton = page.getByRole('button', { name: 'Commands' })

View File

@ -1,23 +1,16 @@
import { test, expect } from '@playwright/test' import { test, expect } from './zoo-test'
import { getUtils, setup, tearDown } from './test-utils' import { getUtils } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Copilot ghost text', () => { test.describe('Copilot ghost text', () => {
// eslint-disable-next-line jest/valid-title // eslint-disable-next-line jest/valid-title
test.skip(true, 'Needs to get covered again') test.skip(true, 'Needs to get covered again')
test('completes code in empty file', async ({ page }) => { test('completes code in empty file', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -52,12 +45,13 @@ test.describe('Copilot ghost text', () => {
test.skip('copilot disabled in sketch mode no select plane', async ({ test.skip('copilot disabled in sketch mode no select plane', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -101,12 +95,13 @@ test.describe('Copilot ghost text', () => {
test('copilot disabled in sketch mode after selecting plane', async ({ test('copilot disabled in sketch mode after selecting plane', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -184,12 +179,12 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-ghostText')).not.toBeVisible() await expect(page.locator('.cm-ghostText')).not.toBeVisible()
}) })
test('ArrowUp in code rejects the suggestion', async ({ page }) => { test('ArrowUp in code rejects the suggestion', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -212,12 +207,15 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('ArrowDown in code rejects the suggestion', async ({ page }) => { test('ArrowDown in code rejects the suggestion', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -240,12 +238,15 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('ArrowLeft in code rejects the suggestion', async ({ page }) => { test('ArrowLeft in code rejects the suggestion', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -268,12 +269,15 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('ArrowRight in code rejects the suggestion', async ({ page }) => { test('ArrowRight in code rejects the suggestion', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -296,12 +300,12 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('Enter in code scoots it down', async ({ page }) => { test('Enter in code scoots it down', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -326,12 +330,15 @@ test.describe('Copilot ghost text', () => {
) )
}) })
test('Ctrl+shift+z in code rejects the suggestion', async ({ page }) => { test('Ctrl+shift+z in code rejects the suggestion', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -360,12 +367,13 @@ test.describe('Copilot ghost text', () => {
test('Ctrl+z in code rejects the suggestion and undos the last code', async ({ test('Ctrl+z in code rejects the suggestion and undos the last code', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await page.waitForTimeout(800) await page.waitForTimeout(800)
await u.codeLocator.click() await u.codeLocator.click()
@ -420,15 +428,17 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
// TODO when we make codemirror a widget, we can test this. // TODO when we make codemirror a widget, we can test this.
//await expect(page.locator('.cm-content')).toHaveText(``) //await expect(page.locator('.cm-content')).toHaveText(``) })
})
test('delete in code rejects the suggestion', async ({ page }) => { test('delete in code rejects the suggestion', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -453,12 +463,15 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('backspace in code rejects the suggestion', async ({ page }) => { test('backspace in code rejects the suggestion', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -483,12 +496,15 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('focus outside code pane rejects the suggestion', async ({ page }) => { test('focus outside code pane rejects the suggestion', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -514,4 +530,5 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
}) })
})
}) })

View File

@ -1,14 +1,6 @@
import { test, expect } from '@playwright/test' import { test, expect } from './zoo-test'
import { getUtils, setup, tearDown } from './test-utils' import { getUtils } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
function countNewlines(input: string): number { function countNewlines(input: string): number {
let count = 0 let count = 0
@ -24,13 +16,14 @@ test.describe('Debug pane', () => {
test('Artifact IDs in the artifact graph are stable across code edits', async ({ test('Artifact IDs in the artifact graph are stable across code edits', async ({
page, page,
context, context,
homePage,
}) => { }) => {
const code = `sketch001 = startSketchOn('XZ') const code = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([1, 1], %) |> line([1, 1], %)
` `
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
const tree = page.getByTestId('debug-feature-tree') const tree = page.getByTestId('debug-feature-tree')
const segment = tree.locator('li', { const segment = tree.locator('li', {
@ -39,7 +32,7 @@ test.describe('Debug pane', () => {
}) })
await test.step('Test setup', async () => { await test.step('Test setup', async () => {
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.openKclCodePanel() await u.openKclCodePanel()
await u.openDebugPanel() await u.openDebugPanel()
// Set the code in the code editor. // Set the code in the code editor.

View File

@ -1,39 +1,31 @@
import { test, expect } from '@playwright/test' import { test, expect } from './zoo-test'
import { join } from 'path' import path from 'path'
import { import {
getUtils, getUtils,
setupElectron,
tearDown,
executorInputPath, executorInputPath,
getPlaywrightDownloadDir,
} from './test-utils' } from './test-utils'
import fsp from 'fs/promises' import fsp from 'fs/promises'
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test( test(
'export works on the first try', 'export works on the first try',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ page, context }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo, const bracketDir = path.join(dir, 'bracket')
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })]) await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
await Promise.all([ await Promise.all([
fsp.copyFile( fsp.copyFile(
executorInputPath('router-template-slate.kcl'), executorInputPath('router-template-slate.kcl'),
join(bracketDir, 'other.kcl') path.join(bracketDir, 'other.kcl')
), ),
fsp.copyFile( fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl') path.join(bracketDir, 'main.kcl')
), ),
]) ])
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -93,12 +85,16 @@ test(
await expect(successToastMessage).toBeVisible() await expect(successToastMessage).toBeVisible()
await expect(exportingToastMessage).not.toBeVisible() await expect(exportingToastMessage).not.toBeVisible()
const firstFileFullPath = path.resolve(
getPlaywrightDownloadDir(page),
exportFileName
)
await test.step('Check the export size', async () => { await test.step('Check the export size', async () => {
await expect await expect
.poll( .poll(
async () => { async () => {
try { try {
const outputGltf = await fsp.readFile(exportFileName) const outputGltf = await fsp.readFile(firstFileFullPath)
return outputGltf.byteLength return outputGltf.byteLength
} catch (e) { } catch (e) {
return 0 return 0
@ -107,9 +103,6 @@ test(
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBeGreaterThan(300_000) .toBeGreaterThan(300_000)
// clean up exported file
await fsp.rm(exportFileName)
}) })
}) })
@ -170,12 +163,16 @@ test(
expect(exportingToastMessage).not.toBeVisible(), expect(exportingToastMessage).not.toBeVisible(),
])) ]))
const secondFileFullPath = path.resolve(
getPlaywrightDownloadDir(page),
exportFileName
)
await test.step('Check the export size', async () => { await test.step('Check the export size', async () => {
await expect await expect
.poll( .poll(
async () => { async () => {
try { try {
const outputGltf = await fsp.readFile(exportFileName) const outputGltf = await fsp.readFile(secondFileFullPath)
return outputGltf.byteLength return outputGltf.byteLength
} catch (e) { } catch (e) {
return 0 return 0
@ -184,13 +181,7 @@ test(
{ timeout: 15_000 } { timeout: 15_000 }
) )
.toBeGreaterThan(100_000) .toBeGreaterThan(100_000)
// clean up exported file
await fsp.rm(exportFileName)
}) })
await electronApp.close()
}) })
await electronApp.close()
} }
) )

View File

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test' import { test, expect } from './zoo-test'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { import {
@ -6,26 +6,16 @@ import {
darkModePlaneColorXZ, darkModePlaneColorXZ,
executorInputPath, executorInputPath,
getUtils, getUtils,
setup,
setupElectron,
tearDown,
} from './test-utils' } from './test-utils'
import { join } from 'path' import { join } from 'path'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Editor tests', () => { test.describe('Editor tests', () => {
test('can comment out code with ctrl+/', async ({ page }) => { test('can comment out code with ctrl+/', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -66,11 +56,12 @@ test.describe('Editor tests', () => {
test('if you click the format button it formats your code', async ({ test('if you click the format button it formats your code', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -96,11 +87,12 @@ test.describe('Editor tests', () => {
test('if you click the format button it formats your code and executes so lints are still there', async ({ test('if you click the format button it formats your code and executes so lints are still there', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -151,9 +143,7 @@ test.describe('Editor tests', () => {
).toBeVisible() ).toBeVisible()
}) })
test('fold gutters work', async ({ page }) => { test('fold gutters work', async ({ page, homePage }) => {
const u = await getUtils(page)
const fullCode = `sketch001 = startSketchOn('XY') const fullCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
@ -171,9 +161,9 @@ test.describe('Editor tests', () => {
|> close(%)` |> close(%)`
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// TODO: Jess needs to fix this but you have to mod the code to get them to show // TODO: Jess needs to fix this but you have to mod the code to get them to show
// up, its an annoying codemirror thing. // up, its an annoying codemirror thing.
@ -224,7 +214,10 @@ test.describe('Editor tests', () => {
await expect(foldGutterFoldLine).not.toBeVisible() await expect(foldGutterFoldLine).not.toBeVisible()
}) })
test('hover over functions shows function description', async ({ page }) => { test('hover over functions shows function description', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -237,9 +230,9 @@ test.describe('Editor tests', () => {
|> close(%)` |> close(%)`
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -268,6 +261,7 @@ test.describe('Editor tests', () => {
test('if you use the format keyboard binding it formats your code', async ({ test('if you use the format keyboard binding it formats your code', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -282,9 +276,9 @@ test.describe('Editor tests', () => {
) )
localStorage.setItem('disableAxis', 'true') localStorage.setItem('disableAxis', 'true')
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -310,6 +304,7 @@ test.describe('Editor tests', () => {
test('if you use the format keyboard binding it formats your code and executes so lints are shown', async ({ test('if you use the format keyboard binding it formats your code and executes so lints are shown', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -324,9 +319,9 @@ test.describe('Editor tests', () => {
) )
localStorage.setItem('disableAxis', 'true') localStorage.setItem('disableAxis', 'true')
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -369,11 +364,14 @@ test.describe('Editor tests', () => {
).toBeVisible() ).toBeVisible()
}) })
test('if you write kcl with lint errors you get lints', async ({ page }) => { test('if you write kcl with lint errors you get lints', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible()
@ -409,7 +407,10 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-info')).not.toBeVisible()
}) })
test('if you fixup kcl errors you clear lints', async ({ page }) => { test('if you fixup kcl errors you clear lints', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -423,9 +424,9 @@ test.describe('Editor tests', () => {
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -447,11 +448,14 @@ test.describe('Editor tests', () => {
).not.toBeVisible() ).not.toBeVisible()
}) })
test('if you write invalid kcl you get inlined errors', async ({ page }) => { test('if you write invalid kcl you get inlined errors', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -518,7 +522,10 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
}) })
test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => { test.fixme('error with 2 source ranges gets 2 diagnostics', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -540,9 +547,11 @@ test.describe('Editor tests', () => {
` `
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -568,7 +577,7 @@ test.describe('Editor tests', () => {
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page.keyboard.type(`extrusion = startSketchOn('XY') await page.keyboard.type(`extrusion = startSketchOn('XY')
|> circle({ center = [0, 0], radius = dia/2 }, %) |> circle({ center: [0, 0], radius: dia/2 }, %)
|> hole(squareHole(length, width, height), %) |> hole(squareHole(length, width, height), %)
|> extrude(height, %)`) |> extrude(height, %)`)
@ -583,10 +592,11 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2) await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2)
}) })
test('if your kcl gets an error from the engine it is inlined', async ({ test('if your kcl gets an error from the engine it is inlined', async ({
context,
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) await context.addInitScript(async () => {
await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`box = startSketchOn('XY') `box = startSketchOn('XY')
@ -604,17 +614,16 @@ test.describe('Editor tests', () => {
|> line([0, -10], %) |> line([0, -10], %)
|> close(%) |> close(%)
|> revolve({ |> revolve({
axis = revolveAxis, axis: revolveAxis,
angle = 90 angle: 90
}, %) }, %)
` `
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await page.goto('/') await homePage.goToModelingScene()
await u.waitForPageLoad()
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
@ -625,12 +634,15 @@ test.describe('Editor tests', () => {
await expect(page.getByText(searchText)).toBeVisible() await expect(page.getByText(searchText)).toBeVisible()
}) })
test.describe('Autocomplete works', () => { test.describe('Autocomplete works', () => {
test('with enter/click to accept the completion', async ({ page }) => { test('with enter/click to accept the completion', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// tests clicking on an option, selection the first option // tests clicking on an option, selection the first option
// and arrowing down to an option // and arrowing down to an option
@ -699,12 +711,12 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0) await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0)
}) })
test('with tab to accept the completion', async ({ page }) => { test('with tab to accept the completion', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// this test might be brittle as we add and remove functions // this test might be brittle as we add and remove functions
// but should also be easy to update. // but should also be easy to update.
@ -770,9 +782,13 @@ test.describe('Editor tests', () => {
|> xLine(5, %) // lin`) |> xLine(5, %) // lin`)
}) })
}) })
test('Can undo a click and point extrude with ctrl+z', async ({ page }) => { test('Can undo a click and point extrude with ctrl+z', async ({
page,
context,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await context.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
@ -783,9 +799,9 @@ test.describe('Editor tests', () => {
) )
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -844,7 +860,10 @@ test.describe('Editor tests', () => {
|> close(%)`) |> close(%)`)
}) })
test('Can undo a sketch modification with ctrl+z', async ({ page }) => { test('Can undo a sketch modification with ctrl+z', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -858,9 +877,9 @@ test.describe('Editor tests', () => {
) )
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -887,7 +906,7 @@ test.describe('Editor tests', () => {
}) })
await page.waitForTimeout(100) await page.waitForTimeout(100)
const startPX = [665, 397] const startPX = [1200 / 2, 500 / 2]
const dragPX = 40 const dragPX = 40
@ -901,9 +920,9 @@ test.describe('Editor tests', () => {
await expect(page.getByTestId('segment-overlay')).toHaveCount(2) await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
// drag startProfieAt handle // drag startProfileAt handle
await page.dragAndDrop('#stream', '#stream', { await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0], y: startPX[1] }, sourcePosition: { x: startPX[0] + 68, y: startPX[1] + 147 },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX }, targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
}) })
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -941,12 +960,12 @@ test.describe('Editor tests', () => {
// expect the code to have changed // expect the code to have changed
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -12.68], %) |> startProfileAt([2.71, -2.71], %)
|> line([15.39, -2.78], %) |> line([15.4, -2.78], %)
|> tangentialArcTo([27.6, -3.05], %) |> tangentialArcTo([27.6, -3.05], %)
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
`) `)
// Hit undo // Hit undo
await page.keyboard.down('Control') await page.keyboard.down('Control')
@ -955,8 +974,8 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -12.68], %) |> startProfileAt([2.71, -2.71], %)
|> line([15.39, -2.78], %) |> line([15.4, -2.78], %)
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close(%) |> close(%)
|> extrude(5, %)`) |> extrude(5, %)`)
@ -968,12 +987,12 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -12.68], %) |> startProfileAt([2.71, -2.71], %)
|> line([12.73, -0.09], %) |> line([12.73, -0.09], %)
|> tangentialArcTo([24.95, -0.38], %) |> tangentialArcTo([24.95, -0.38], %)
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
`) `)
// Hit undo again. // Hit undo again.
await page.keyboard.down('Control') await page.keyboard.down('Control')
@ -993,10 +1012,8 @@ test.describe('Editor tests', () => {
test.fixme( test.fixme(
`Can use the import stdlib function on a local OBJ file`, `Can use the import stdlib function on a local OBJ file`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ page, context }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'cube') const bracketDir = join(dir, 'cube')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
@ -1004,10 +1021,10 @@ test.describe('Editor tests', () => {
join(bracketDir, 'cube.obj') join(bracketDir, 'cube.obj')
) )
await fsp.writeFile(join(bracketDir, 'main.kcl'), '') await fsp.writeFile(join(bracketDir, 'main.kcl'), '')
},
}) })
const viewportSize = { width: 1200, height: 500 } const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize) await page.setBodyDimensions(viewportSize)
// Locators and constants // Locators and constants
const u = await getUtils(page) const u = await getUtils(page)
@ -1065,8 +1082,6 @@ test.describe('Editor tests', () => {
}) })
.toBeGreaterThan(15) .toBeGreaterThan(15)
}) })
await electronApp.close()
} }
) )
}) })

File diff suppressed because it is too large Load Diff

View File

@ -54,13 +54,13 @@ export class EditorFixture {
} }
} }
if (!shouldNormalise) { if (!shouldNormalise) {
const expectStart = expect(this.codeContent) const expectStart = expect.poll(() => this.codeContent.textContent())
if (not) { if (not) {
const result = await expectStart.not.toContainText(code, { timeout }) const result = await expectStart.not.toContain(code)
await resetPane() await resetPane()
return result return result
} }
const result = await expectStart.toContainText(code, { timeout }) const result = await expectStart.toContain(code)
await resetPane() await resetPane()
return result return result
} }
@ -147,4 +147,20 @@ export class EditorFixture {
openPane() { openPane() {
return openPane(this.page, this.paneButtonTestId) return openPane(this.page, this.paneButtonTestId)
} }
scrollToText(text: string) {
return this.page.evaluate((scrollToText: string) => {
// editorManager is available on the window object.
// @ts-ignore
let index = editorManager._editorView.docView.view.state.doc
.toString()
.indexOf(scrollToText)
// @ts-ignore
editorManager._editorView.dispatch({
selection: {
anchor: index,
},
scrollIntoView: true,
})
}, text)
}
} }

View File

@ -1,11 +1,11 @@
import type { import type {
BrowserContext, BrowserContext,
ElectronApplication, ElectronApplication,
Page,
TestInfo, TestInfo,
Page,
} from '@playwright/test' } from '@playwright/test'
import { test as base } from '@playwright/test'
import { getUtils, setup, setupElectron, tearDown } from '../test-utils' import { getUtils, setup, setupElectron } from '../test-utils'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { join } from 'path' import { join } from 'path'
import { CmdBarFixture } from './cmdBarFixture' import { CmdBarFixture } from './cmdBarFixture'
@ -20,11 +20,11 @@ export class AuthenticatedApp {
public readonly page: Page public readonly page: Page
public readonly context: BrowserContext public readonly context: BrowserContext
public readonly testInfo: TestInfo public readonly testInfo: TestInfo
public readonly viewPortSize = { width: 1000, height: 500 } public readonly viewPortSize = { width: 1200, height: 500 }
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) { constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this.page = page
this.context = context this.context = context
this.page = page
this.testInfo = testInfo this.testInfo = testInfo
} }
@ -49,9 +49,7 @@ export class AuthenticatedApp {
} }
} }
interface Fixtures { export interface Fixtures {
app: AuthenticatedApp
tronApp: AuthenticatedTronApp
cmdBar: CmdBarFixture cmdBar: CmdBarFixture
editor: EditorFixture editor: EditorFixture
toolbar: ToolbarFixture toolbar: ToolbarFixture
@ -61,9 +59,11 @@ interface Fixtures {
export class AuthenticatedTronApp { export class AuthenticatedTronApp {
public readonly _page: Page public readonly _page: Page
public page: Page public page: Page
public readonly context: BrowserContext public context: BrowserContext
public readonly testInfo: TestInfo public readonly testInfo: TestInfo
public electronApp?: ElectronApplication public electronApp?: ElectronApplication
public readonly viewPortSize = { width: 1200, height: 500 }
public dir: string = ''
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) { constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this._page = page this._page = page
@ -79,15 +79,24 @@ export class AuthenticatedTronApp {
appSettings?: Partial<SaveSettingsPayload> appSettings?: Partial<SaveSettingsPayload>
} = { fixtures: {} } } = { fixtures: {} }
) { ) {
const { electronApp, page } = await setupElectron({ const { electronApp, page, context, dir } = await setupElectron({
testInfo: this.testInfo, testInfo: this.testInfo,
folderSetupFn: arg.folderSetupFn, folderSetupFn: arg.folderSetupFn,
cleanProjectDir: arg.cleanProjectDir, cleanProjectDir: arg.cleanProjectDir,
appSettings: arg.appSettings, appSettings: arg.appSettings,
}) })
this.page = page this.page = page
this.context = context
this.electronApp = electronApp this.electronApp = electronApp
await page.setViewportSize({ width: 1200, height: 500 }) this.dir = dir
// Easier to access throughout utils
this.page.dir = dir
// Setup localStorage, addCookies, reload
await setup(this.context, this.page, this.testInfo)
await page.setViewportSize(this.viewPortSize)
for (const key of unsafeTypedKeys(arg.fixtures)) { for (const key of unsafeTypedKeys(arg.fixtures)) {
const fixture = arg.fixtures[key] const fixture = arg.fixtures[key]
@ -110,32 +119,20 @@ export class AuthenticatedTronApp {
}) })
} }
export const test = base.extend<Fixtures>({ export const fixtures = {
app: async ({ page, context }, use, testInfo) => { cmdBar: async ({ page }: { page: Page }, use: any) => {
await use(new AuthenticatedApp(context, page, testInfo))
},
tronApp: async ({ page, context }, use, testInfo) => {
await use(new AuthenticatedTronApp(context, page, testInfo))
},
cmdBar: async ({ page }, use) => {
await use(new CmdBarFixture(page)) await use(new CmdBarFixture(page))
}, },
editor: async ({ page }, use) => { editor: async ({ page }: { page: Page }, use: any) => {
await use(new EditorFixture(page)) await use(new EditorFixture(page))
}, },
toolbar: async ({ page }, use) => { toolbar: async ({ page }: { page: Page }, use: any) => {
await use(new ToolbarFixture(page)) await use(new ToolbarFixture(page))
}, },
scene: async ({ page }, use) => { scene: async ({ page }: { page: Page }, use: any) => {
await use(new SceneFixture(page)) await use(new SceneFixture(page))
}, },
homePage: async ({ page }, use) => { homePage: async ({ page }: { page: Page }, use: any) => {
await use(new HomePageFixture(page)) await use(new HomePageFixture(page))
}, },
}) }
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
export { expect } from '@playwright/test'

View File

@ -4,7 +4,6 @@ import { expect } from '@playwright/test'
interface ProjectCardState { interface ProjectCardState {
title: string title: string
fileCount: number fileCount: number
folderCount: number
} }
interface HomePageState { interface HomePageState {
@ -15,10 +14,14 @@ interface HomePageState {
export class HomePageFixture { export class HomePageFixture {
public page: Page public page: Page
projectSection!: Locator
projectCard!: Locator projectCard!: Locator
projectCardTitle!: Locator projectCardTitle!: Locator
projectCardFile!: Locator projectCardFile!: Locator
projectCardFolder!: Locator projectCardFolder!: Locator
projectButtonNew!: Locator
projectButtonContinue!: Locator
projectTextName!: Locator
sortByDateBtn!: Locator sortByDateBtn!: Locator
sortByNameBtn!: Locator sortByNameBtn!: Locator
@ -29,11 +32,19 @@ export class HomePageFixture {
reConstruct = (page: Page) => { reConstruct = (page: Page) => {
this.page = page this.page = page
this.projectSection = this.page.getByTestId('home-section')
this.projectCard = this.page.getByTestId('project-link') this.projectCard = this.page.getByTestId('project-link')
this.projectCardTitle = this.page.getByTestId('project-title') this.projectCardTitle = this.page.getByTestId('project-title')
this.projectCardFile = this.page.getByTestId('project-file-count') this.projectCardFile = this.page.getByTestId('project-file-count')
this.projectCardFolder = this.page.getByTestId('project-folder-count') this.projectCardFolder = this.page.getByTestId('project-folder-count')
this.projectButtonNew = this.page.getByTestId('home-new-file')
this.projectTextName = this.page.getByTestId('cmd-bar-arg-value')
this.projectButtonContinue = this.page.getByRole('button', {
name: 'Continue',
})
this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified') this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified')
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name') this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
} }
@ -61,15 +72,13 @@ export class HomePageFixture {
const projectCards = await this.projectCard.all() const projectCards = await this.projectCard.all()
const projectCardStates: Array<ProjectCardState> = [] const projectCardStates: Array<ProjectCardState> = []
for (const projectCard of projectCards) { for (const projectCard of projectCards) {
const [title, fileCount, folderCount] = await Promise.all([ const [title, fileCount] = await Promise.all([
(await projectCard.locator(this.projectCardTitle).textContent()) || '', (await projectCard.locator(this.projectCardTitle).textContent()) || '',
Number(await projectCard.locator(this.projectCardFile).textContent()), Number(await projectCard.locator(this.projectCardFile).textContent()),
Number(await projectCard.locator(this.projectCardFolder).textContent()),
]) ])
projectCardStates.push({ projectCardStates.push({
title: title, title: title,
fileCount, fileCount,
folderCount,
}) })
} }
return projectCardStates return projectCardStates
@ -94,10 +103,25 @@ export class HomePageFixture {
.toEqual(expectedState) .toEqual(expectedState)
} }
createAndGoToProject = async (projectTitle: string) => {
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
await this.projectButtonNew.click()
await this.projectTextName.click()
await this.projectTextName.fill(projectTitle)
await this.projectButtonContinue.click()
}
openProject = async (projectTitle: string) => { openProject = async (projectTitle: string) => {
const projectCard = this.projectCard.locator( const projectCard = this.projectCard.locator(
this.page.getByText(projectTitle) this.page.getByText(projectTitle)
) )
await projectCard.click() await projectCard.click()
} }
goToModelingScene = async (name: string = 'testDefault') => {
// On web this is a no-op. There is no project view.
if (process.env.PLATFORM === 'web') return
await this.createAndGoToProject(name)
}
} }

View File

@ -28,6 +28,7 @@ type SceneSerialised = {
type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean> type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean> type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean>
type DblClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean> type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean>
type DragFromHandler = ( type DragFromHandler = (
dragParams: mouseDragFromParams dragParams: mouseDragFromParams
@ -52,8 +53,9 @@ export class SceneFixture {
expectState = async (expected: SceneSerialised) => { expectState = async (expected: SceneSerialised) => {
return expect return expect
.poll(() => this._serialiseScene(), { .poll(async () => await this._serialiseScene(), {
message: `Expected scene state to match`, intervals: [1_000, 2_000, 10_000],
timeout: 60000,
}) })
.toEqual(expected) .toEqual(expected)
} }
@ -68,7 +70,7 @@ export class SceneFixture {
x: number, x: number,
y: number, y: number,
{ steps }: { steps: number } = { steps: 20 } { steps }: { steps: number } = { steps: 20 }
): [ClickHandler, MoveHandler] => ): [ClickHandler, MoveHandler, DblClickHandler] =>
[ [
(clickParams?: mouseParams) => { (clickParams?: mouseParams) => {
if (clickParams?.pixelDiff) { if (clickParams?.pixelDiff) {
@ -90,6 +92,16 @@ export class SceneFixture {
} }
return this.page.mouse.move(x, y, { steps }) return this.page.mouse.move(x, y, { steps })
}, },
(clickParams?: mouseParams) => {
if (clickParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
() => this.page.mouse.dblclick(x, y),
clickParams.pixelDiff
)
}
return this.page.mouse.dblclick(x, y)
},
] as const ] as const
makeDragHelpers = ( makeDragHelpers = (
x: number, x: number,
@ -176,7 +188,10 @@ export class SceneFixture {
type: 'default_camera_get_settings', type: 'default_camera_get_settings',
}, },
}) })
await this.waitForExecutionDone() await this.page
.locator(`[data-receive-command-type="default_camera_get_settings"]`)
.first()
.waitFor()
const position = await Promise.all([ const position = await Promise.all([
this.page.getByTestId('cam-x-position').inputValue().then(Number), this.page.getByTestId('cam-x-position').inputValue().then(Number),
this.page.getByTestId('cam-y-position').inputValue().then(Number), this.page.getByTestId('cam-y-position').inputValue().then(Number),
@ -227,6 +242,7 @@ export class SceneFixture {
} }
async clickGizmoMenuItem(name: string) { async clickGizmoMenuItem(name: string) {
await this.gizmo.hover()
await this.gizmo.click({ button: 'right' }) await this.gizmo.click({ button: 'right' })
const buttonToTest = this.page.getByRole('button', { const buttonToTest = this.page.getByRole('button', {
name: name, name: name,

View File

@ -1,11 +1,12 @@
import type { Page, Locator } from '@playwright/test' import type { Page, Locator } from '@playwright/test'
import { expect } from './fixtureSetup' import { expect } from '../zoo-test'
import { doAndWaitForImageDiff } from '../test-utils' import { doAndWaitForImageDiff } from '../test-utils'
export class ToolbarFixture { export class ToolbarFixture {
public page: Page public page: Page
extrudeButton!: Locator extrudeButton!: Locator
loftButton!: Locator
offsetPlaneButton!: Locator offsetPlaneButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
lineBtn!: Locator lineBtn!: Locator
@ -26,6 +27,7 @@ export class ToolbarFixture {
reConstruct = (page: Page) => { reConstruct = (page: Page) => {
this.page = page this.page = page
this.extrudeButton = page.getByTestId('extrude') this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.offsetPlaneButton = page.getByTestId('plane-offset') this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch') this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line') this.lineBtn = page.getByTestId('line')

View File

@ -1,29 +1,22 @@
import { test, expect } from '@playwright/test' import { test, expect } from './zoo-test'
import { setupElectron, tearDown, executorInputPath } from './test-utils' import { executorInputPath } from './test-utils'
import { join } from 'path' import { join } from 'path'
import fsp from 'fs/promises' import fsp from 'fs/promises'
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test( test(
'When machine-api server not found butt is disabled and shows the reason', 'When machine-api server not found butt is disabled and shows the reason',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket') const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl') join(bracketDir, 'main.kcl')
) )
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
@ -47,28 +40,23 @@ test(
// that the machine-api server is not found // that the machine-api server is not found
await makeButton.hover() await makeButton.hover()
await expect(page.getByText(notFoundText).first()).toBeVisible() await expect(page.getByText(notFoundText).first()).toBeVisible()
await electronApp.close()
} }
) )
test( test(
'When machine-api server not found home screen & project status shows the reason', 'When machine-api server not found home screen & project status shows the reason',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket') const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl') join(bracketDir, 'main.kcl')
) )
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
const notFoundText = 'Machine API server was not discovered' const notFoundText = 'Machine API server was not discovered'
@ -91,7 +79,5 @@ test(
await networkMachineToggle.hover() await networkMachineToggle.hover()
await expect(page.getByText(notFoundText).nth(1)).toBeVisible() await expect(page.getByText(notFoundText).nth(1)).toBeVisible()
await electronApp.close()
} }
) )

View File

@ -0,0 +1,12 @@
// These tests are meant to simply test starting and stopping the electron
// application, check it can make it to the project pane, and nothing more.
// It also tests our test wrappers are working.
// Additionally this serves as a nice minimal example.
import { test, expect } from './zoo-test'
test.describe('Open the application', () => {
test('see the project view', async ({ page, context }) => {
await expect(page.getByTestId('home-section')).toBeVisible()
})
})

View File

@ -1,79 +1,63 @@
import { test, expect } from '@playwright/test' import { test, expect } from './zoo-test'
import { join } from 'path' import { join } from 'path'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { import { getUtils, executorInputPath, createProject } from './test-utils'
getUtils,
setup,
setupElectron,
tearDown,
executorInputPath,
createProject,
} from './test-utils'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { import {
TEST_SETTINGS_KEY, TEST_SETTINGS_KEY,
TEST_SETTINGS_ONBOARDING_START, TEST_SETTINGS_ONBOARDING_START,
TEST_SETTINGS_ONBOARDING_EXPORT, TEST_SETTINGS_ONBOARDING_EXPORT,
TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING,
TEST_SETTINGS_ONBOARDING_USER_MENU, TEST_SETTINGS_ONBOARDING_USER_MENU,
} from './storageStates' } from './storageStates'
import * as TOML from '@iarna/toml' import * as TOML from '@iarna/toml'
test.beforeEach(async ({ context, page }, testInfo) => { // Because onboarding relies on an app setting we need to set it as incompletel
if (testInfo.tags.includes('@electron')) { // for all these tests.
return
}
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Onboarding tests', () => { test.describe('Onboarding tests', () => {
test('Onboarding code is shown in the editor', async ({ page }) => {
const u = await getUtils(page)
// Override beforeEach test setup
await page.addInitScript(
async ({ settingsKey }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.removeItem('persistCode')
localStorage.removeItem(settingsKey)
},
{ settingsKey: TEST_SETTINGS_KEY }
)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
})
test( test(
'Desktop: fresh onboarding executes and loads', 'Onboarding code is shown in the editor',
{ tag: '@electron' }, {
async ({ browserName: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
appSettings: { appSettings: {
app: { app: {
onboardingStatus: 'incomplete', onboardingStatus: 'incomplete',
}, },
}, },
cleanProjectDir: true, cleanProjectDir: true,
}) },
async ({ context, page, homePage }) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
}
)
test(
'Desktop: fresh onboarding executes and loads',
{
tag: '@electron',
appSettings: {
app: {
onboardingStatus: 'incomplete',
},
},
cleanProjectDir: true,
},
async ({ page, homePage }, testInfo) => {
const u = await getUtils(page) const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 } const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize) await page.setBodyDimensions(viewportSize)
await test.step(`Create a project and open to the onboarding`, async () => { await test.step(`Create a project and open to the onboarding`, async () => {
await createProject({ name: 'project-link', page }) await createProject({ name: 'project-link', page })
@ -93,60 +77,71 @@ test.describe('Onboarding tests', () => {
'// Shelf Bracket' '// Shelf Bracket'
) )
}) })
await electronApp.close()
} }
) )
test('Code resets after confirmation', async ({ page }) => { test(
'Code resets after confirmation',
{
appSettings: {
app: {
onboardingStatus: 'incomplete',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
const initialCode = `sketch001 = startSketchOn('XZ')` const initialCode = `sketch001 = startSketchOn('XZ')`
// Load the page up with some code so we see the confirmation warning // Load the page up with some code so we see the confirmation warning
// when we go to replay onboarding // when we go to replay onboarding
await page.addInitScript((code) => { await context.addInitScript((code) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
}, initialCode) }, initialCode)
const u = await getUtils(page) await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 }) await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
// Replay the onboarding // Replay the onboarding
await page.getByRole('link', { name: 'Settings' }).last().click() await page.getByRole('link', { name: 'Settings' }).last().click()
const replayButton = page.getByRole('button', { name: 'Replay onboarding' }) const replayButton = page.getByRole('button', {
name: 'Replay onboarding',
})
await expect(replayButton).toBeVisible() await expect(replayButton).toBeVisible()
await replayButton.click() await replayButton.click()
// Ensure we see the warning, and that the code has not yet updated // Ensure we see the warning, and that the code has not yet updated
await expect( await expect(page.getByText('Would you like to create')).toBeVisible()
page.getByText('Replaying onboarding resets your code')
).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(initialCode) await expect(page.locator('.cm-content')).toHaveText(initialCode)
const nextButton = page.getByTestId('onboarding-next') const nextButton = page.getByTestId('onboarding-next')
await expect(nextButton).toBeVisible() await nextButton.hover()
await nextButton.click() await nextButton.click()
// Ensure we see the introduction and that the code has been reset // Ensure we see the introduction and that the code has been reset
await expect(page.getByText('Welcome to Modeling App!')).toBeVisible() await expect(page.getByText('Welcome to Modeling App!')).toBeVisible()
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket') await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
// Ensure we persisted the code to local storage. // There used to be old code here that checked if we stored the reset
// Playwright's addInitScript method unfortunately will reset // code into localStorage but that isnt the case on desktop. It gets
// this code if we try reloading the page as a test, // saved to the file system, which we have other tests for.
// so this is our best way to test persistence afaik. }
expect( )
await page.evaluate(() => {
return localStorage.getItem('persistCode')
})
).toContain('// Shelf Bracket')
})
test('Click through each onboarding step', async ({ page }) => {
const u = await getUtils(page)
test(
'Click through each onboarding step',
{
appSettings: {
app: {
onboardingStatus: 'incomplete',
},
},
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup // Override beforeEach test setup
await page.addInitScript( await context.addInitScript(
async ({ settingsKey, settings }) => { async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately // Give no initial code, so that the onboarding start is shown immediately
localStorage.setItem('persistCode', '') localStorage.setItem('persistCode', '')
@ -154,107 +149,113 @@ test.describe('Onboarding tests', () => {
}, },
{ {
settingsKey: TEST_SETTINGS_KEY, settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_START }), settings: TOML.stringify({
settings: TEST_SETTINGS_ONBOARDING_START,
}),
} }
) )
await page.setViewportSize({ width: 1200, height: 1080 }) await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
// Test that the onboarding pane loaded // Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible() await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
const nextButton = page.getByTestId('onboarding-next') const nextButton = page.getByTestId('onboarding-next')
while ((await nextButton.innerText()) !== 'Finish') { while ((await nextButton.innerText()) !== 'Finish') {
await expect(nextButton).toBeVisible() await nextButton.hover()
await nextButton.click() await nextButton.click()
} }
// Finish the onboarding // Finish the onboarding
await expect(nextButton).toBeVisible() await nextButton.hover()
await nextButton.click() await nextButton.click()
// Test that the onboarding pane is gone // Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible() await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect(page.url()).not.toContain('onboarding') await expect.poll(() => page.url()).not.toContain('/onboarding')
})
test('Onboarding redirects and code updating', async ({ page }) => {
const u = await getUtils(page)
// Override beforeEach test setup
await page.addInitScript(
async ({ settingsKey, settings }) => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', 'sigmaAllow = 15000')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_EXPORT }),
} }
) )
await page.setViewportSize({ width: 1200, height: 500 }) test(
'Onboarding redirects and code updating',
{
appSettings: {
app: {
onboardingStatus: '/export',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
const originalCode = 'sigmaAllow = 15000'
await u.waitForAuthSkipAppStart()
// Test that the redirect happened
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
)
// Test that you come back to this page when you refresh
await page.reload()
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/%2Fbrowser%2Fmain.kcl/onboarding/export`
)
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
// Test that the code changes when you advance to the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText('')
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
})
test('Onboarding code gets reset to demo on Interactive Numbers step', async ({
page,
}) => {
test.skip(
process.platform === 'darwin',
"Skip on macOS, because Playwright isn't behaving the same as the actual browser"
)
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
// Override beforeEach test setup // Override beforeEach test setup
await page.addInitScript( await context.addInitScript(
async ({ settingsKey, settings, badCode }) => { async ({ settingsKey, settings }) => {
localStorage.setItem('persistCode', badCode) // Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', originalCode)
localStorage.setItem(settingsKey, settings) localStorage.setItem(settingsKey, settings)
}, },
{ {
settingsKey: TEST_SETTINGS_KEY, settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: TOML.stringify({
settings: TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING, settings: TEST_SETTINGS_ONBOARDING_EXPORT,
}), }),
badCode,
} }
) )
await page.setViewportSize({ width: 1200, height: 1080 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await page.waitForURL('**' + onboardingPaths.PARAMETRIC_MODELING, { // Test that the redirect happened
waitUntil: 'domcontentloaded', await expect.poll(() => page.url()).toContain('/onboarding/export')
})
// Test that you come back to this page when you refresh
await page.reload()
await expect.poll(() => page.url()).toContain('/onboarding/export')
// Test that the code changes when you advance to the next step
await page.getByTestId('onboarding-next').hover()
await page.getByTestId('onboarding-next').click()
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
await expect(page.locator('.cm-content')).not.toHaveText(originalCode)
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
}
)
test(
'Onboarding code gets reset to demo on Interactive Numbers step',
{
appSettings: {
app: {
onboardingStatus: '/parametric-modeling',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
await expect
.poll(() => page.url())
.toContain(onboardingPaths.PARAMETRIC_MODELING)
const bracketNoNewLines = bracket.replace(/\n/g, '') const bracketNoNewLines = bracket.replace(/\n/g, '')
@ -270,6 +271,7 @@ test.describe('Onboarding tests', () => {
await expect(u.codeLocator).toHaveText(badCode) await expect(u.codeLocator).toHaveText(badCode)
// Click to the next step // Click to the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click() await page.locator('[data-testid="onboarding-next"]').click()
await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, { await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, {
waitUntil: 'domcontentloaded', waitUntil: 'domcontentloaded',
@ -277,13 +279,25 @@ test.describe('Onboarding tests', () => {
// Check that the code has been reset // Check that the code has been reset
await expect(u.codeLocator).toHaveText(bracketNoNewLines) await expect(u.codeLocator).toHaveText(bracketNoNewLines)
}) }
)
test('Avatar text updates depending on image load success', async ({ // (lee) The two avatar tests are weird because even on main, we don't have
page, // anything to do with the avatar inside the onboarding test. Due to the
}) => { // low impact of an avatar not showing I'm changing this to fixme.
test.fixme(
'Avatar text updates depending on image load success',
{
appSettings: {
app: {
onboardingStatus: 'incomplete',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup // Override beforeEach test setup
await page.addInitScript( await context.addInitScript(
async ({ settingsKey, settings }) => { async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings) localStorage.setItem(settingsKey, settings)
}, },
@ -295,11 +309,8 @@ test.describe('Onboarding tests', () => {
} }
) )
const u = await getUtils(page) await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 }) await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
// Test that the text in this step is correct // Test that the text in this step is correct
const avatarLocator = await page const avatarLocator = await page
@ -327,13 +338,16 @@ test.describe('Onboarding tests', () => {
}) })
// 404 the CI avatar image // 404 the CI avatar image
await page.route('https://lh3.googleusercontent.com/**', async (route) => { await page.route(
'https://lh3.googleusercontent.com/**',
async (route) => {
await route.fulfill({ await route.fulfill({
status: 404, status: 404,
contentType: 'text/plain', contentType: 'text/plain',
body: 'Not Found!', body: 'Not Found!',
}) })
}) }
)
await page.reload({ waitUntil: 'domcontentloaded' }) await page.reload({ waitUntil: 'domcontentloaded' })
@ -341,13 +355,22 @@ test.describe('Onboarding tests', () => {
await expect(avatarLocator).not.toBeVisible() await expect(avatarLocator).not.toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible() await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('the menu button') await expect(onboardingOverlayLocator).toContainText('the menu button')
}) }
)
test("Avatar text doesn't mention avatar when no avatar", async ({ test.fixme(
page, "Avatar text doesn't mention avatar when no avatar",
}) => { {
appSettings: {
app: {
onboardingStatus: 'incomplete',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup // Override beforeEach test setup
await page.addInitScript( await context.addInitScript(
async ({ settingsKey, settings }) => { async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings) localStorage.setItem(settingsKey, settings)
localStorage.setItem('FORCE_NO_IMAGE', 'FORCE_NO_IMAGE') localStorage.setItem('FORCE_NO_IMAGE', 'FORCE_NO_IMAGE')
@ -360,11 +383,8 @@ test.describe('Onboarding tests', () => {
} }
) )
const u = await getUtils(page) await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 }) await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
// Test that the text in this step is correct // Test that the text in this step is correct
const sidebar = page.getByTestId('user-sidebar-toggle') const sidebar = page.getByTestId('user-sidebar-toggle')
@ -390,23 +410,28 @@ test.describe('Onboarding tests', () => {
for (const feature of userMenuFeatures) { for (const feature of userMenuFeatures) {
await expect(onboardingOverlayLocator).toContainText(feature) await expect(onboardingOverlayLocator).toContainText(feature)
} }
}) }
)
}) })
test( test(
'Restarting onboarding on desktop takes one attempt', 'Restarting onboarding on desktop takes one attempt',
{ tag: '@electron' }, {
async ({ browser: _ }, testInfo) => { appSettings: {
const { electronApp, page } = await setupElectron({ app: {
testInfo, onboardingStatus: 'dismissed',
folderSetupFn: async (dir) => { },
},
cleanProjectDir: true,
},
async ({ context, page, homePage }, testInfo) => {
await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate') const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true }) await fsp.mkdir(routerTemplateDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('router-template-slate.kcl'), executorInputPath('router-template-slate.kcl'),
join(routerTemplateDir, 'main.kcl') join(routerTemplateDir, 'main.kcl')
) )
},
}) })
// Our constants // Our constants
@ -418,9 +443,8 @@ test(
const restartOnboardingButton = page.getByRole('button', { const restartOnboardingButton = page.getByRole('button', {
name: 'Reset onboarding', name: 'Reset onboarding',
}) })
const restartConfirmationButton = page.getByRole('button', { const nextButton = page.getByTestId('onboarding-next')
name: 'Make a new project',
})
const tutorialProjectIndicator = page const tutorialProjectIndicator = page
.getByTestId('project-sidebar-toggle') .getByTestId('project-sidebar-toggle')
.filter({ hasText: 'Tutorial Project 00' }) .filter({ hasText: 'Tutorial Project 00' })
@ -439,7 +463,7 @@ test(
}) })
await test.step('Navigate into project', async () => { await test.step('Navigate into project', async () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -455,8 +479,8 @@ test(
await helpMenuButton.click() await helpMenuButton.click()
await restartOnboardingButton.click() await restartOnboardingButton.click()
await expect(restartConfirmationButton).toBeVisible() await nextButton.hover()
await restartConfirmationButton.click() await nextButton.click()
}) })
await test.step('Confirm that the onboarding has restarted', async () => { await test.step('Confirm that the onboarding has restarted', async () => {
@ -480,11 +504,9 @@ test(
await restartOnboardingSettingsButton.click() await restartOnboardingSettingsButton.click()
// Since the code is empty, we should not see the confirmation dialog // Since the code is empty, we should not see the confirmation dialog
await expect(restartConfirmationButton).not.toBeVisible() await expect(nextButton).not.toBeVisible()
await expect(tutorialProjectIndicator).toBeVisible() await expect(tutorialProjectIndicator).toBeVisible()
await expect(tutorialModalText).toBeVisible() await expect(tutorialModalText).toBeVisible()
}) })
await electronApp.close()
} }
) )

View File

@ -1,20 +1,34 @@
import { test, expect, AuthenticatedApp } from './fixtures/fixtureSetup' import { test, expect, Page } from './zoo-test'
import { EditorFixture } from './fixtures/editorFixture' import { EditorFixture } from './fixtures/editorFixture'
import { SceneFixture } from './fixtures/sceneFixture' import { SceneFixture } from './fixtures/sceneFixture'
import { ToolbarFixture } from './fixtures/toolbarFixture' import { ToolbarFixture } from './fixtures/toolbarFixture'
import fs from 'node:fs/promises'
import path from 'node:path'
import { getUtils } from './test-utils'
// test file is for testing point an click code gen functionality that's not sketch mode related // test file is for testing point an click code gen functionality that's not sketch mode related
test( test('verify extruding circle works', async ({
'verify extruding circle works', context,
{ tag: ['@skipWin'] }, homePage,
async ({ app, cmdBar, editor, toolbar, scene }) => { cmdBar,
test.skip( editor,
process.platform === 'win32', toolbar,
'Fails on windows in CI, can not be replicated locally on windows.' scene,
}) => {
const file = await fs.readFile(
path.resolve(
__dirname,
'../../',
'./src/wasm-lib/tests/executor/inputs/test-circle-extrude.kcl'
),
'utf-8'
) )
const file = await app.getInputFile('test-circle-extrude.kcl') await context.addInitScript((file) => {
await app.initialise(file) localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217) const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217)
await test.step('because there is sweepable geometry, verify extrude is enable when nothing is selected', async () => { await test.step('because there is sweepable geometry, verify extrude is enable when nothing is selected', async () => {
@ -27,7 +41,17 @@ test(
const circleSnippet = const circleSnippet =
'circle({ center = [318.33, 168.1], radius = 182.8 }, %)' 'circle({ center = [318.33, 168.1], radius = 182.8 }, %)'
await editor.expectState({ await editor.expectState({
activeLines: [], activeLines: ["constsketch002=startSketchOn('XZ')"],
highlightedCode: circleSnippet,
diagnostics: [],
})
await test.step('check code model connection works and that button is still enable once circle is selected ', async () => {
await moveToCircle()
const circleSnippet =
'circle({ center = [318.33, 168.1], radius = 182.8 }, %)'
await editor.expectState({
activeLines: ["constsketch002=startSketchOn('XZ')"],
highlightedCode: circleSnippet, highlightedCode: circleSnippet,
diagnostics: [], diagnostics: [],
}) })
@ -40,6 +64,8 @@ test(
}) })
await expect(toolbar.extrudeButton).toBeEnabled() await expect(toolbar.extrudeButton).toBeEnabled()
}) })
await expect(toolbar.extrudeButton).toBeEnabled()
})
await test.step('do extrude flow and check extrude code is added to editor', async () => { await test.step('do extrude flow and check extrude code is added to editor', async () => {
await toolbar.extrudeButton.click() await toolbar.extrudeButton.click()
@ -66,13 +92,12 @@ test(
await editor.expectEditor.toContain(expectString) await editor.expectEditor.toContain(expectString)
}) })
} })
)
test.describe('verify sketch on chamfer works', () => { test.describe('verify sketch on chamfer works', () => {
const _sketchOnAChamfer = const _sketchOnAChamfer =
( (
app: AuthenticatedApp, page: Page,
editor: EditorFixture, editor: EditorFixture,
toolbar: ToolbarFixture, toolbar: ToolbarFixture,
scene: SceneFixture scene: SceneFixture
@ -124,7 +149,7 @@ test.describe('verify sketch on chamfer works', () => {
await toolbar.startSketchPlaneSelection() await toolbar.startSketchPlaneSelection()
await clickChamfer() await clickChamfer()
// timeout wait for engine animation is unavoidable // timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600) await page.waitForTimeout(1000)
await editor.expectEditor.toContain(afterChamferSelectSnippet) await editor.expectEditor.toContain(afterChamferSelectSnippet)
}) })
await test.step('make sure a basic sketch can be added', async () => { await test.step('make sure a basic sketch can be added', async () => {
@ -135,7 +160,9 @@ test.describe('verify sketch on chamfer works', () => {
pixelDiff: 50, pixelDiff: 50,
}) })
await rectangle2ndClick() await rectangle2ndClick()
await editor.expectEditor.toContain(afterRectangle2ndClickSnippet) await editor.expectEditor.toContain(afterRectangle2ndClickSnippet, {
shouldNormalise: true,
})
}) })
await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => { await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
@ -150,18 +177,29 @@ test.describe('verify sketch on chamfer works', () => {
}) })
}) })
} }
test( test('works on all edge selections and can break up multi edges in a chamfer array', async ({
'works on all edge selections and can break up multi edges in a chamfer array', context,
{ tag: ['@skipWin'] }, page,
async ({ app, editor, toolbar, scene }) => { homePage,
test.skip( editor,
process.platform === 'win32', toolbar,
'Fails on windows in CI, can not be replicated locally on windows.' scene,
}) => {
const file = await fs.readFile(
path.resolve(
__dirname,
'../../',
'./src/wasm-lib/tests/executor/inputs/e2e-can-sketch-on-chamfer.kcl'
),
'utf-8'
) )
const file = await app.getInputFile('e2e-can-sketch-on-chamfer.kcl') await context.addInitScript((file) => {
await app.initialise(file) localStorage.setItem('persistCode', file)
}, file)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
const sketchOnAChamfer = _sketchOnAChamfer(app, editor, toolbar, scene) const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
await sketchOnAChamfer({ await sketchOnAChamfer({
clickCoords: { x: 570, y: 220 }, clickCoords: { x: 570, y: 220 },
@ -175,8 +213,7 @@ test.describe('verify sketch on chamfer works', () => {
getOppositeEdge(seg01) getOppositeEdge(seg01)
]}, %)`, ]}, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)',
'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
@ -207,8 +244,7 @@ test.describe('verify sketch on chamfer works', () => {
] ]
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)',
'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)', afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([ |> angledLine([
@ -233,9 +269,8 @@ test.describe('verify sketch on chamfer works', () => {
getNextAdjacentEdge(seg02) getNextAdjacentEdge(seg02)
] ]
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)',
'sketch003 = startSketchOn(extrude001, seg04)', afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA003) - 90, segAng(rectangleSegmentA003) - 90,
@ -257,8 +292,7 @@ test.describe('verify sketch on chamfer works', () => {
length = 30, length = 30,
tags = [getNextAdjacentEdge(yo)] tags = [getNextAdjacentEdge(yo)]
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet: 'sketch005 = startSketchOn(extrude001, seg06)',
'sketch005 = startSketchOn(extrude001, seg06)',
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)', afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005) afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
@ -305,7 +339,7 @@ test.describe('verify sketch on chamfer works', () => {
tags = [getNextAdjacentEdge(yo)] tags = [getNextAdjacentEdge(yo)]
}, %, $seg06) }, %, $seg06)
sketch005 = startSketchOn(extrude001, seg06) sketch005 = startSketchOn(extrude001, seg06)
|> startProfileAt([-23.43, 19.69], %) |> startProfileAt([-23.43,19.69], %)
|> angledLine([0, 9.1], %, $rectangleSegmentA005) |> angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA005) - 90, segAng(rectangleSegmentA005) - 90,
@ -318,7 +352,7 @@ test.describe('verify sketch on chamfer works', () => {
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch004 = startSketchOn(extrude001, seg05) sketch004 = startSketchOn(extrude001, seg05)
|> startProfileAt([82.57, 322.96], %) |> startProfileAt([82.57,322.96], %)
|> angledLine([0, 11.16], %, $rectangleSegmentA004) |> angledLine([0, 11.16], %, $rectangleSegmentA004)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA004) - 90, segAng(rectangleSegmentA004) - 90,
@ -331,7 +365,7 @@ test.describe('verify sketch on chamfer works', () => {
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch003 = startSketchOn(extrude001, seg04) sketch003 = startSketchOn(extrude001, seg04)
|> startProfileAt([-209.64, 255.28], %) |> startProfileAt([-209.64,255.28], %)
|> angledLine([0, 11.56], %, $rectangleSegmentA003) |> angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA003) - 90, segAng(rectangleSegmentA003) - 90,
@ -344,7 +378,7 @@ test.describe('verify sketch on chamfer works', () => {
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch002 = startSketchOn(extrude001, seg03) sketch002 = startSketchOn(extrude001, seg03)
|> startProfileAt([205.96, 254.59], %) |> startProfileAt([205.96,254.59], %)
|> angledLine([0, 11.39], %, $rectangleSegmentA002) |> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002) - 90, segAng(rectangleSegmentA002) - 90,
@ -360,23 +394,31 @@ test.describe('verify sketch on chamfer works', () => {
{ shouldNormalise: true } { shouldNormalise: true }
) )
}) })
} })
)
test( test('Works on chamfers that are non in a pipeExpression can break up multi edges in a chamfer array', async ({
'Works on chamfers that are non in a pipeExpression can break up multi edges in a chamfer array', context,
{ tag: ['@skipWin'] }, page,
async ({ app, editor, toolbar, scene }) => { homePage,
test.skip( editor,
process.platform === 'win32', toolbar,
'Fails on windows in CI, can not be replicated locally on windows.' scene,
}) => {
const file = await fs.readFile(
path.resolve(
__dirname,
'../../',
'./src/wasm-lib/tests/executor/inputs/e2e-can-sketch-on-chamfer-no-pipeExpr.kcl'
),
'utf-8'
) )
const file = await app.getInputFile( await context.addInitScript((file) => {
'e2e-can-sketch-on-chamfer-no-pipeExpr.kcl' localStorage.setItem('persistCode', file)
) }, file)
await app.initialise(file) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
const sketchOnAChamfer = _sketchOnAChamfer(app, editor, toolbar, scene) const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
await sketchOnAChamfer({ await sketchOnAChamfer({
clickCoords: { x: 570, y: 220 }, clickCoords: { x: 570, y: 220 },
@ -390,8 +432,7 @@ test.describe('verify sketch on chamfer works', () => {
getOppositeEdge(seg01) getOppositeEdge(seg01)
]}, extrude001)`, ]}, extrude001)`,
beforeChamferSnippetEnd: '}, extrude001)', beforeChamferSnippetEnd: '}, extrude001)',
afterChamferSelectSnippet: afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)',
'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)', afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
@ -448,48 +489,54 @@ sketch002 = startSketchOn(extrude001, seg03)
`, `,
{ shouldNormalise: true } { shouldNormalise: true }
) )
} })
)
}) })
test(`Verify axis, origin, and horizontal snapping`, async ({ test(`Verify axis, origin, and horizontal snapping`, async ({
app, page,
homePage,
editor, editor,
toolbar, toolbar,
scene, scene,
}) => { }) => {
const viewPortSize = { width: 1200, height: 500 }
await page.setBodyDimensions(viewPortSize)
await homePage.goToModelingScene()
// Constants and locators // Constants and locators
// These are mappings from screenspace to KCL coordinates, // These are mappings from screenspace to KCL coordinates,
// until we merge in our coordinate system helpers // until we merge in our coordinate system helpers
const xzPlane = [ const xzPlane = [
app.viewPortSize.width * 0.65, viewPortSize.width * 0.65,
app.viewPortSize.height * 0.3, viewPortSize.height * 0.3,
] as const ] as const
const originSloppy = { const originSloppy = {
screen: [ screen: [
app.viewPortSize.width / 2 + 3, // 3px off the center of the screen viewPortSize.width / 2 + 3, // 3px off the center of the screen
app.viewPortSize.height / 2, viewPortSize.height / 2,
], ],
kcl: [0, 0], kcl: [0, 0],
} as const } as const
const xAxisSloppy = { const xAxisSloppy = {
screen: [ screen: [
app.viewPortSize.width * 0.75, viewPortSize.width * 0.75,
app.viewPortSize.height / 2 - 3, // 3px off the X-axis viewPortSize.height / 2 - 3, // 3px off the X-axis
], ],
kcl: [16.95, 0], kcl: [20.34, 0],
} as const } as const
const offYAxis = { const offYAxis = {
screen: [ screen: [
app.viewPortSize.width * 0.6, // Well off the Y-axis, out of snapping range viewPortSize.width * 0.6, // Well off the Y-axis, out of snapping range
app.viewPortSize.height * 0.3, viewPortSize.height * 0.3,
], ],
kcl: [6.78, 6.78], kcl: [8.14, 6.78],
} as const } as const
const yAxisSloppy = { const yAxisSloppy = {
screen: [ screen: [
app.viewPortSize.width / 2 + 5, // 5px off the Y-axis viewPortSize.width / 2 + 5, // 5px off the Y-axis
app.viewPortSize.height * 0.3, viewPortSize.height * 0.3,
], ],
kcl: [0, 6.78], kcl: [0, 6.78],
} as const } as const
@ -510,15 +557,13 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`, afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
} }
await app.initialise()
await test.step(`Start a sketch on the XZ plane`, async () => { await test.step(`Start a sketch on the XZ plane`, async () => {
await editor.closePane() await editor.closePane()
await toolbar.startSketchPlaneSelection() await toolbar.startSketchPlaneSelection()
await moveToXzPlane() await moveToXzPlane()
await clickOnXzPlane() await clickOnXzPlane()
// timeout wait for engine animation is unavoidable // timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600) await page.waitForTimeout(600)
await editor.expectEditor.toContain(expectedCodeSnippets.sketchOnXzPlane) await editor.expectEditor.toContain(expectedCodeSnippets.sketchOnXzPlane)
}) })
await test.step(`Place a point a few pixels off the middle, verify it still snaps to 0,0`, async () => { await test.step(`Place a point a few pixels off the middle, verify it still snaps to 0,0`, async () => {
@ -552,20 +597,127 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
}) })
}) })
test(`Verify user can double-click to edit a sketch`, async ({
context,
page,
homePage,
editor,
toolbar,
scene,
}) => {
const u = await getUtils(page)
const initialCode = `closedSketch = startSketchOn('XZ')
|> circle({ center = [8, 5], radius = 2 }, %)
openSketch = startSketchOn('XY')
|> startProfileAt([-5, 0], %)
|> lineTo([0, 5], %)
|> xLine(5, %)
|> tangentialArcTo([10, 0], %)
`
const viewPortSize = { width: 1000, height: 500 }
await page.setBodyDimensions(viewPortSize)
await context.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
const pointInsideCircle = {
x: viewPortSize.width * 0.63,
y: viewPortSize.height * 0.5,
}
const pointOnPathAfterSketching = {
x: viewPortSize.width * 0.65,
y: viewPortSize.height * 0.5,
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_clickOpenPath, moveToOpenPath, dblClickOpenPath] =
scene.makeMouseHelpers(
pointOnPathAfterSketching.x,
pointOnPathAfterSketching.y
)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_clickCircle, moveToCircle, dblClickCircle] = scene.makeMouseHelpers(
pointInsideCircle.x,
pointInsideCircle.y
)
const exitSketch = async () => {
await test.step(`Exit sketch mode`, async () => {
await toolbar.exitSketchBtn.click()
await expect(toolbar.exitSketchBtn).not.toBeVisible()
await expect(toolbar.startSketchBtn).toBeEnabled()
})
}
await test.step(`Double-click on the closed sketch`, async () => {
await moveToCircle()
await dblClickCircle()
await expect(toolbar.startSketchBtn).not.toBeVisible()
await expect(toolbar.exitSketchBtn).toBeVisible()
await editor.expectState({
activeLines: [`|>circle({center=[8,5],radius=2},%)`],
highlightedCode: 'circle({center=[8,5],radius=2},%)',
diagnostics: [],
})
})
await page.waitForTimeout(1000)
await exitSketch()
await page.waitForTimeout(1000)
// Drag the sketch line out of the axis view which blocks the click
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: {
x: viewPortSize.width * 0.7,
y: viewPortSize.height * 0.5,
},
targetPosition: {
x: viewPortSize.width * 0.7,
y: viewPortSize.height * 0.4,
},
})
await page.waitForTimeout(500)
await test.step(`Double-click on the open sketch`, async () => {
await moveToOpenPath()
await scene.expectPixelColor([250, 250, 250], pointOnPathAfterSketching, 15)
// There is a full execution after exiting sketch that clears the scene.
await page.waitForTimeout(500)
await dblClickOpenPath()
await expect(toolbar.startSketchBtn).not.toBeVisible()
await expect(toolbar.exitSketchBtn).toBeVisible()
// Wait for enter sketch mode to complete
await page.waitForTimeout(500)
await editor.expectState({
activeLines: [`|>tangentialArcTo([10,0],%)`],
highlightedCode: 'tangentialArcTo([10,0],%)',
diagnostics: [],
})
})
})
test(`Offset plane point-and-click`, async ({ test(`Offset plane point-and-click`, async ({
app, context,
page,
homePage,
scene, scene,
editor, editor,
toolbar, toolbar,
cmdBar, cmdBar,
}) => { }) => {
await app.initialise()
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 150 } const testPoint = { x: 700, y: 150 }
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const expectedOutput = `plane001 = offsetPlane('XZ', 5)` const expectedOutput = `plane001 = offsetPlane('XZ', 5)`
await homePage.goToModelingScene()
await test.step(`Look for the blue of the XZ plane`, async () => { await test.step(`Look for the blue of the XZ plane`, async () => {
await scene.expectPixelColor([50, 51, 96], testPoint, 15) await scene.expectPixelColor([50, 51, 96], testPoint, 15)
}) })
@ -601,3 +753,104 @@ test(`Offset plane point-and-click`, async ({
await scene.expectPixelColor([74, 74, 74], testPoint, 15) await scene.expectPixelColor([74, 74, 74], testPoint, 15)
}) })
}) })
const loftPointAndClickCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },
]
loftPointAndClickCases.forEach(({ shouldPreselect }) => {
test(`Loft point-and-click (preselected sketches: ${shouldPreselect})`, async ({
context,
homePage,
page,
scene,
editor,
toolbar,
cmdBar,
}) => {
const u = await getUtils(page)
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50)
sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %)
`
await page.setBodyDimensions({ width: 1000, height: 500 })
await context.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(
testPoint.x,
testPoint.y + 80
)
const loftDeclaration = 'loft001 = loft([sketch001, sketch002])'
await test.step(`Look for the white of the sketch001 shape`, async () => {
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
async function selectSketches() {
await clickOnSketch1()
await page.keyboard.down('Shift')
await clickOnSketch2()
await page.waitForTimeout(500)
await page.keyboard.up('Shift')
}
if (!shouldPreselect) {
await test.step(`Go through the command bar flow without preselected sketches`, async () => {
await toolbar.loftButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: { Selection: '' },
highlightedHeaderArg: 'selection',
commandName: 'Loft',
})
await selectSketches()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Selection: '2 faces' },
commandName: 'Loft',
})
await cmdBar.progressCmdBar()
})
} else {
await test.step(`Preselect the two sketches`, async () => {
await selectSketches()
})
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
await toolbar.loftButton.click()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Selection: '2 faces' },
commandName: 'Loft',
})
await cmdBar.progressCmdBar()
})
}
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(loftDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [loftDeclaration],
highlightedCode: '',
})
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
})
})
})

View File

@ -1,49 +1,41 @@
import { test, expect } from '@playwright/test' import { test, expect } from './zoo-test'
import { import {
doExport, doExport,
executorInputPath, executorInputPath,
getUtils, getUtils,
isOutOfViewInScrollContainer, isOutOfViewInScrollContainer,
Paths, Paths,
setupElectron,
tearDown,
createProject, createProject,
getPlaywrightDownloadDir,
} from './test-utils' } from './test-utils'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import fs from 'fs' import fs from 'fs'
import { join } from 'path' import path from 'path'
import { DEFAULT_PROJECT_KCL_FILE } from 'lib/constants' import { DEFAULT_PROJECT_KCL_FILE } from 'lib/constants'
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test( test(
'projects reload if a new one is created, deleted, or renamed externally', 'projects reload if a new one is created, deleted, or renamed externally',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
let externalCreatedProjectName = 'external-created-project' let externalCreatedProjectName = 'external-created-project'
let targetDir = '' let targetDir = ''
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
targetDir = dir targetDir = dir
setTimeout(() => { setTimeout(() => {
const myDir = join(dir, externalCreatedProjectName) const myDir = path.join(dir, externalCreatedProjectName)
;(async () => { ;(async () => {
await fsp.mkdir(myDir) await fsp.mkdir(myDir)
await fsp.writeFile( await fsp.writeFile(
join(myDir, DEFAULT_PROJECT_KCL_FILE), path.join(myDir, DEFAULT_PROJECT_KCL_FILE),
'sca ba be bop de day wawa skee' 'sca ba be bop de day wawa skee'
) )
})().catch(console.error) })().catch(console.error)
}, 5000) }, 5000)
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
const projectLinks = page.getByTestId('project-link') const projectLinks = page.getByTestId('project-link')
@ -51,34 +43,27 @@ test(
await expect(projectLinks).toContainText(externalCreatedProjectName) await expect(projectLinks).toContainText(externalCreatedProjectName)
await fsp.rename( await fsp.rename(
join(targetDir, externalCreatedProjectName), path.join(targetDir, externalCreatedProjectName),
join(targetDir, externalCreatedProjectName + '1') path.join(targetDir, externalCreatedProjectName + '1')
) )
externalCreatedProjectName += '1' externalCreatedProjectName += '1'
await expect(projectLinks).toContainText(externalCreatedProjectName) await expect(projectLinks).toContainText(externalCreatedProjectName)
await fsp.rm(join(targetDir, externalCreatedProjectName), { await fsp.rm(path.join(targetDir, externalCreatedProjectName), {
recursive: true, recursive: true,
force: true, force: true,
}) })
await expect(projectLinks).toHaveCount(0) await expect(projectLinks).toHaveCount(0)
await electronApp.close()
} }
) )
test( test(
'click help/keybindings from home page', 'click help/keybindings from home page',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await page.setBodyDimensions({ width: 1200, height: 500 })
testInfo,
folderSetupFn: async () => {},
})
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -89,28 +74,23 @@ test(
await page.getByTestId('keybindings-button').click() await page.getByTestId('keybindings-button').click()
// Make sure the keyboard shortcuts modal is visible. // Make sure the keyboard shortcuts modal is visible.
await expect(page.getByText('Enter Sketch Mode')).toBeVisible() await expect(page.getByText('Enter Sketch Mode')).toBeVisible()
await electronApp.close()
} }
) )
test( test(
'click help/keybindings from project page', 'click help/keybindings from project page',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo, const bracketDir = path.join(dir, 'bracket')
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl') path.join(bracketDir, 'main.kcl')
) )
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -131,27 +111,22 @@ test(
await page.getByTestId('keybindings-button').click() await page.getByTestId('keybindings-button').click()
// Make sure the keyboard shortcuts modal is visible. // Make sure the keyboard shortcuts modal is visible.
await expect(page.getByText('Enter Sketch Mode')).toBeVisible() await expect(page.getByText('Enter Sketch Mode')).toBeVisible()
await electronApp.close()
} }
) )
test( test(
'when code with error first loads you get errors in console', 'when code with error first loads you get errors in console',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo, await fsp.mkdir(path.join(dir, 'broken-code'), { recursive: true })
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'broken-code'), { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('broken-code-test.kcl'), executorInputPath('broken-code-test.kcl'),
join(dir, 'broken-code', 'main.kcl') path.join(dir, 'broken-code', 'main.kcl')
) )
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await expect(page.getByText('broken-code')).toBeVisible() await expect(page.getByText('broken-code')).toBeVisible()
@ -169,8 +144,6 @@ test(
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
const crypticErrorText = `Expected a tag declarator` const crypticErrorText = `Expected a tag declarator`
await expect(page.getByText(crypticErrorText).first()).toBeVisible() await expect(page.getByText(crypticErrorText).first()).toBeVisible()
await electronApp.close()
} }
) )
@ -181,20 +154,17 @@ test.describe('Can export from electron app', () => {
test( test(
`Can export using ${method}`, `Can export using ${method}`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo, const bracketDir = path.join(dir, 'bracket')
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl') path.join(bracketDir, 'main.kcl')
) )
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
const u = await getUtils(page) const u = await getUtils(page)
page.on('console', console.log) page.on('console', console.log)
@ -242,12 +212,17 @@ test.describe('Can export from electron app', () => {
) )
}) })
const filepath = path.resolve(
getPlaywrightDownloadDir(page),
'main.gltf'
)
await test.step('Check the export size', async () => { await test.step('Check the export size', async () => {
await expect await expect
.poll( .poll(
async () => { async () => {
try { try {
const outputGltf = await fsp.readFile('main.gltf') const outputGltf = await fsp.readFile(filepath)
return outputGltf.byteLength return outputGltf.byteLength
} catch (e) { } catch (e) {
return 0 return 0
@ -258,10 +233,8 @@ test.describe('Can export from electron app', () => {
.toBeGreaterThan(300_000) .toBeGreaterThan(300_000)
// clean up exported file // clean up exported file
await fsp.rm('main.gltf') await fsp.rm(filepath)
}) })
await electronApp.close()
} }
) )
} }
@ -269,10 +242,8 @@ test.describe('Can export from electron app', () => {
test( test(
'Rename and delete projects, also spam arrow keys when renaming', 'Rename and delete projects, also spam arrow keys when renaming',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }) await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
@ -297,10 +268,9 @@ test(
) )
const _1995 = new Date('1995-01-01T00:03:33') const _1995 = new Date('1995-01-01T00:03:33')
fs.utimesSync(`${dir}/lego/main.kcl`, _1995, _1995) fs.utimesSync(`${dir}/lego/main.kcl`, _1995, _1995)
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -465,26 +435,21 @@ test(
// expect the name not to have changed // expect the name not to have changed
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
}) })
await electronApp.close()
} }
) )
test( test(
'pressing "delete" on home screen should do nothing', 'pressing "delete" on home screen should do nothing',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }) await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/router-template-slate/main.kcl` `${dir}/router-template-slate/main.kcl`
) )
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -498,8 +463,6 @@ test(
// expect to still be on the home page // expect to still be on the home page
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Your Projects')).toBeVisible() await expect(page.getByText('Your Projects')).toBeVisible()
await electronApp.close()
} }
) )
@ -507,17 +470,14 @@ test.describe(`Project management commands`, () => {
test( test(
`Rename from project page`, `Rename from project page`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const projectName = `my_project_to_rename` const projectName = `my_project_to_rename`
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/${projectName}/main.kcl` `${dir}/${projectName}/main.kcl`
) )
},
}) })
const u = await getUtils(page) const u = await getUtils(page)
@ -537,7 +497,7 @@ test.describe(`Project management commands`, () => {
const toastMessage = page.getByText(`Successfully renamed`) const toastMessage = page.getByText(`Successfully renamed`)
await test.step(`Setup`, async () => { await test.step(`Setup`, async () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await projectHomeLink.click() await projectHomeLink.click()
@ -565,25 +525,20 @@ test.describe(`Project management commands`, () => {
await expect(projectHomeLink.first()).toBeVisible() await expect(projectHomeLink.first()).toBeVisible()
await expect(projectHomeLink.first()).toContainText(projectRenamedName) await expect(projectHomeLink.first()).toContainText(projectRenamedName)
}) })
await electronApp.close()
} }
) )
test( test(
`Delete from project page`, `Delete from project page`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName: _ }, testInfo) => { async ({ context, page }, testInfo) => {
const projectName = `my_project_to_delete` const projectName = `my_project_to_delete`
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/${projectName}/main.kcl` `${dir}/${projectName}/main.kcl`
) )
},
}) })
const u = await getUtils(page) const u = await getUtils(page)
@ -600,7 +555,7 @@ test.describe(`Project management commands`, () => {
const noProjectsMessage = page.getByText('No Projects found') const noProjectsMessage = page.getByText('No Projects found')
await test.step(`Setup`, async () => { await test.step(`Setup`, async () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await projectHomeLink.click() await projectHomeLink.click()
@ -622,24 +577,19 @@ test.describe(`Project management commands`, () => {
await test.step(`Check the project was deleted and we navigated home`, async () => { await test.step(`Check the project was deleted and we navigated home`, async () => {
await expect(noProjectsMessage).toBeVisible() await expect(noProjectsMessage).toBeVisible()
}) })
await electronApp.close()
} }
) )
test( test(
`Rename from home page`, `Rename from home page`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName: _ }, testInfo) => { async ({ context, page }, testInfo) => {
const projectName = `my_project_to_rename` const projectName = `my_project_to_rename`
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/${projectName}/main.kcl` `${dir}/${projectName}/main.kcl`
) )
},
}) })
// Constants and locators // Constants and locators
@ -657,7 +607,7 @@ test.describe(`Project management commands`, () => {
const toastMessage = page.getByText(`Successfully renamed`) const toastMessage = page.getByText(`Successfully renamed`)
await test.step(`Setup`, async () => { await test.step(`Setup`, async () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await expect(projectHomeLink).toBeVisible() await expect(projectHomeLink).toBeVisible()
}) })
@ -682,24 +632,19 @@ test.describe(`Project management commands`, () => {
).toBeVisible() ).toBeVisible()
await expect(projectHomeLink).not.toHaveText(projectName) await expect(projectHomeLink).not.toHaveText(projectName)
}) })
await electronApp.close()
} }
) )
test( test(
`Delete from home page`, `Delete from home page`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName: _ }, testInfo) => { async ({ context, page }, testInfo) => {
const projectName = `my_project_to_delete` const projectName = `my_project_to_delete`
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
`${dir}/${projectName}/main.kcl` `${dir}/${projectName}/main.kcl`
) )
},
}) })
// Constants and locators // Constants and locators
@ -715,7 +660,7 @@ test.describe(`Project management commands`, () => {
const noProjectsMessage = page.getByText('No Projects found') const noProjectsMessage = page.getByText('No Projects found')
await test.step(`Setup`, async () => { await test.step(`Setup`, async () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await expect(projectHomeLink).toBeVisible() await expect(projectHomeLink).toBeVisible()
}) })
@ -736,8 +681,6 @@ test.describe(`Project management commands`, () => {
await expect(projectHomeLink).not.toBeVisible() await expect(projectHomeLink).not.toBeVisible()
await expect(noProjectsMessage).toBeVisible() await expect(noProjectsMessage).toBeVisible()
}) })
await electronApp.close()
} }
) )
}) })
@ -745,11 +688,9 @@ test.describe(`Project management commands`, () => {
test( test(
'File in the file pane should open with a single click', 'File in the file pane should open with a single click',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const projectName = 'router-template-slate' const projectName = 'router-template-slate'
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
@ -759,10 +700,9 @@ test(
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl', 'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
`${dir}/${projectName}/otherThingToClickOn.kcl` `${dir}/${projectName}/otherThingToClickOn.kcl`
) )
},
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -786,35 +726,30 @@ test(
await expect(u.codeLocator).toContainText( await expect(u.codeLocator).toContainText(
'A mounting bracket for the Focusrite Scarlett Solo audio interface' 'A mounting bracket for the Focusrite Scarlett Solo audio interface'
) )
await electronApp.close()
} }
) )
test( test(
'Nested directories in project without main.kcl do not create main.kcl', 'Nested directories in project without main.kcl do not create main.kcl',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
let testDir: string | undefined let testDir: string | undefined
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo, await fsp.mkdir(path.join(dir, 'router-template-slate', 'nested'), {
folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'router-template-slate', 'nested'), {
recursive: true, recursive: true,
}) })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('router-template-slate.kcl'), executorInputPath('router-template-slate.kcl'),
join(dir, 'router-template-slate', 'nested', 'slate.kcl') path.join(dir, 'router-template-slate', 'nested', 'slate.kcl')
) )
await fsp.copyFile( await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(dir, 'router-template-slate', 'nested', 'bracket.kcl') path.join(dir, 'router-template-slate', 'nested', 'bracket.kcl')
) )
testDir = dir testDir = dir
},
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -842,44 +777,41 @@ test(
if (testDir !== undefined) { if (testDir !== undefined) {
// eslint-disable-next-line jest/no-conditional-expect // eslint-disable-next-line jest/no-conditional-expect
await expect( await expect(
fsp.access(join(testDir, 'router-template-slate', 'main.kcl')) fsp.access(path.join(testDir, 'router-template-slate', 'main.kcl'))
).rejects.toThrow() ).rejects.toThrow()
// eslint-disable-next-line jest/no-conditional-expect // eslint-disable-next-line jest/no-conditional-expect
await expect( await expect(
fsp.access(join(testDir, 'router-template-slate', 'nested', 'main.kcl')) fsp.access(
path.join(testDir, 'router-template-slate', 'nested', 'main.kcl')
)
).rejects.toThrow() ).rejects.toThrow()
} }
await electronApp.close()
} }
) )
test.fixme( test.fixme(
'Deleting projects, can delete individual project, can still create projects after deleting all', 'Deleting projects, can delete individual project, can still create projects after deleting all',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const projectData = [ const projectData = [
['router-template-slate', 'cylinder.kcl'], ['router-template-slate', 'cylinder.kcl'],
['bracket', 'focusrite_scarlett_mounting_braket.kcl'], ['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
['lego', 'lego.kcl'], ['lego', 'lego.kcl'],
] ]
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
// Do these serially to ensure the order is correct // Do these serially to ensure the order is correct
for (const [name, file] of projectData) { for (const [name, file] of projectData) {
await fsp.mkdir(join(dir, name), { recursive: true }) await fsp.mkdir(path.join(dir, name), { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath(file), executorInputPath(file),
join(dir, name, `main.kcl`) path.join(dir, name, `main.kcl`)
) )
// Wait 1s between each project to ensure the order is correct // Wait 1s between each project to ensure the order is correct
await new Promise((r) => setTimeout(r, 1_000)) await new Promise((r) => setTimeout(r, 1_000))
} }
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await test.step('delete the middle project, i.e. the bracket project', async () => { await test.step('delete the middle project, i.e. the bracket project', async () => {
@ -932,19 +864,15 @@ test.fixme(
page.getByTestId('project-link').filter({ hasText: 'project-000' }) page.getByTestId('project-link').filter({ hasText: 'project-000' })
).toBeVisible() ).toBeVisible()
}) })
await electronApp.close()
} }
) )
test( test(
'Can load a file with CRLF line endings', 'Can load a file with CRLF line endings',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo, const routerTemplateDir = path.join(dir, 'router-template-slate')
folderSetupFn: async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true }) await fsp.mkdir(routerTemplateDir, { recursive: true })
const file = await fsp.readFile( const file = await fsp.readFile(
@ -954,14 +882,13 @@ test(
// Replace both \r optionally so we don't end up with \r\r\n // Replace both \r optionally so we don't end up with \r\r\n
const fileWithCRLF = file.replace(/\r?\n/g, '\r\n') const fileWithCRLF = file.replace(/\r?\n/g, '\r\n')
await fsp.writeFile( await fsp.writeFile(
join(routerTemplateDir, 'main.kcl'), path.join(routerTemplateDir, 'main.kcl'),
fileWithCRLF, fileWithCRLF,
'utf-8' 'utf-8'
) )
},
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -974,37 +901,32 @@ test(
await expect(u.codeLocator).toContainText('routerDiameter') await expect(u.codeLocator).toContainText('routerDiameter')
await expect(u.codeLocator).toContainText('templateGap') await expect(u.codeLocator).toContainText('templateGap')
await expect(u.codeLocator).toContainText('minClampingDistance') await expect(u.codeLocator).toContainText('minClampingDistance')
await electronApp.close()
} }
) )
test( test(
'Can sort projects on home page', 'Can sort projects on home page',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const projectData = [ const projectData = [
['router-template-slate', 'cylinder.kcl'], ['router-template-slate', 'cylinder.kcl'],
['bracket', 'focusrite_scarlett_mounting_braket.kcl'], ['bracket', 'focusrite_scarlett_mounting_braket.kcl'],
['lego', 'lego.kcl'], ['lego', 'lego.kcl'],
] ]
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
// Do these serially to ensure the order is correct // Do these serially to ensure the order is correct
for (const [name, file] of projectData) { for (const [name, file] of projectData) {
await fsp.mkdir(join(dir, name), { recursive: true }) await fsp.mkdir(path.join(dir, name), { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath(file), executorInputPath(file),
join(dir, name, `main.kcl`) path.join(dir, name, `main.kcl`)
) )
// Wait 1s between each project to ensure the order is correct // Wait 1s between each project to ensure the order is correct
await new Promise((r) => setTimeout(r, 1_000)) await new Promise((r) => setTimeout(r, 1_000))
} }
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
const getAllProjects = () => page.getByTestId('project-link').all() const getAllProjects = () => page.getByTestId('project-link').all()
@ -1086,18 +1008,15 @@ test(
) )
} }
}) })
await electronApp.close()
} }
) )
test.fixme( test.fixme(
'When the project folder is empty, user can create new project and open it.', 'When the project folder is empty, user can create new project and open it.',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ page }, testInfo) => {
const { electronApp, page } = await setupElectron({ testInfo })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -1180,24 +1099,21 @@ extrude001 = extrude(200, sketch001)`)
await createProject({ name, page, returnHome: true }) await createProject({ name, page, returnHome: true })
await expect(projectLinks.getByText(name)).toBeVisible() await expect(projectLinks.getByText(name)).toBeVisible()
} }
await electronApp.close()
} }
) )
test( test(
'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)', 'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
await Promise.all([ await Promise.all([
fsp.mkdir(join(dir, 'router-template-slate'), { recursive: true }), fsp.mkdir(path.join(dir, 'router-template-slate'), { recursive: true }),
fsp.mkdir(join(dir, 'bracket'), { recursive: true }), fsp.mkdir(path.join(dir, 'bracket'), { recursive: true }),
]) ])
await Promise.all([ await Promise.all([
fsp.copyFile( fsp.copyFile(
join( path.join(
'src', 'src',
'wasm-lib', 'wasm-lib',
'tests', 'tests',
@ -1205,10 +1121,10 @@ test(
'inputs', 'inputs',
'router-template-slate.kcl' 'router-template-slate.kcl'
), ),
join(dir, 'router-template-slate', 'main.kcl') path.join(dir, 'router-template-slate', 'main.kcl')
), ),
fsp.copyFile( fsp.copyFile(
join( path.join(
'src', 'src',
'wasm-lib', 'wasm-lib',
'tests', 'tests',
@ -1216,13 +1132,12 @@ test(
'inputs', 'inputs',
'focusrite_scarlett_mounting_braket.kcl' 'focusrite_scarlett_mounting_braket.kcl'
), ),
join(dir, 'bracket', 'main.kcl') path.join(dir, 'bracket', 'main.kcl')
), ),
]) ])
},
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -1281,18 +1196,14 @@ test(
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible() await expect(page.getByText('New Project')).toBeVisible()
}) })
await electronApp.close()
} }
) )
test( test(
'You can change the root projects directory and nothing is lost', 'You can change the root projects directory and nothing is lost',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page, electronApp }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
await Promise.all([ await Promise.all([
fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }), fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }),
fsp.mkdir(`${dir}/bracket`, { recursive: true }), fsp.mkdir(`${dir}/bracket`, { recursive: true }),
@ -1307,9 +1218,8 @@ test(
`${dir}/bracket/main.kcl` `${dir}/bracket/main.kcl`
), ),
]) ])
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -1335,8 +1245,7 @@ test(
.locator('section#projectDirectory input') .locator('section#projectDirectory input')
.inputValue() .inputValue()
// Can't use Playwright filechooser since this is happening in electron. const handleFile = electronApp?.evaluate(
const handleFile = electronApp.evaluate(
async ({ dialog }, filePaths) => { async ({ dialog }, filePaths) => {
dialog.showOpenDialog = () => dialog.showOpenDialog = () =>
Promise.resolve({ canceled: false, filePaths }) Promise.resolve({ canceled: false, filePaths })
@ -1346,9 +1255,9 @@ test(
await page.getByTestId('project-directory-button').click() await page.getByTestId('project-directory-button').click()
await handleFile await handleFile
await expect(page.locator('section#projectDirectory input')).toHaveValue( await expect
newProjectDirName .poll(() => page.locator('section#projectDirectory input').inputValue())
) .toContain(newProjectDirName)
await page.getByTestId('settings-close-button').click() await page.getByTestId('settings-close-button').click()
@ -1366,7 +1275,7 @@ test(
await page.getByTestId('project-directory-settings-link').click() await page.getByTestId('project-directory-settings-link').click()
const handleFile = electronApp.evaluate( const handleFile = electronApp?.evaluate(
async ({ dialog }, filePaths) => { async ({ dialog }, filePaths) => {
dialog.showOpenDialog = () => dialog.showOpenDialog = () =>
Promise.resolve({ canceled: false, filePaths }) Promise.resolve({ canceled: false, filePaths })
@ -1387,15 +1296,13 @@ test(
await expect(page.getByText('bracket')).toBeVisible() await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
}) })
await electronApp.close()
} }
) )
test( test(
'Search projects on desktop home', 'Search projects on desktop home',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName: _ }, testInfo) => { async ({ context, page }, testInfo) => {
const projectData = [ const projectData = [
['basic bracket', 'focusrite_scarlett_mounting_braket.kcl'], ['basic bracket', 'focusrite_scarlett_mounting_braket.kcl'],
['basic-cube', 'basic_fillet_cube_end.kcl'], ['basic-cube', 'basic_fillet_cube_end.kcl'],
@ -1403,20 +1310,17 @@ test(
['router-template-slate', 'router-template-slate.kcl'], ['router-template-slate', 'router-template-slate.kcl'],
['Ancient Temple Block', 'lego.kcl'], ['Ancient Temple Block', 'lego.kcl'],
] ]
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
// Do these serially to ensure the order is correct // Do these serially to ensure the order is correct
for (const [name, file] of projectData) { for (const [name, file] of projectData) {
await fsp.mkdir(join(dir, name), { recursive: true }) await fsp.mkdir(path.join(dir, name), { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath(file), executorInputPath(file),
join(dir, name, `main.kcl`) path.join(dir, name, `main.kcl`)
) )
} }
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -1448,19 +1352,15 @@ test(
await expect(page.getByText(name)).toBeVisible() await expect(page.getByText(name)).toBeVisible()
} }
}) })
await electronApp.close()
} }
) )
test( test(
'file pane is scrollable when there are many files', 'file pane is scrollable when there are many files',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo, const testDir = path.join(dir, 'testProject')
folderSetupFn: async (dir) => {
const testDir = join(dir, 'testProject')
await fsp.mkdir(testDir, { recursive: true }) await fsp.mkdir(testDir, { recursive: true })
const fileNames = [ const fileNames = [
'angled_line.kcl', 'angled_line.kcl',
@ -1526,13 +1426,12 @@ test(
for (const fileName of fileNames) { for (const fileName of fileNames) {
await fsp.copyFile( await fsp.copyFile(
executorInputPath(fileName), executorInputPath(fileName),
join(testDir, fileName) path.join(testDir, fileName)
) )
} }
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -1558,30 +1457,25 @@ test(
false false
) )
}) })
await electronApp.close()
} }
) )
test( test(
'select all in code editor does not actually select all, just what is visible (regression)', 'select all in code editor does not actually select all, just what is visible (regression)',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ context, page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo,
folderSetupFn: async (dir) => {
// src/wasm-lib/tests/executor/inputs/mike_stress_test.kcl // src/wasm-lib/tests/executor/inputs/mike_stress_test.kcl
const name = 'mike_stress_test' const name = 'mike_stress_test'
const testDir = join(dir, name) const testDir = path.join(dir, name)
await fsp.mkdir(testDir, { recursive: true }) await fsp.mkdir(testDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath(`${name}.kcl`), executorInputPath(`${name}.kcl`),
join(testDir, 'main.kcl') path.join(testDir, 'main.kcl')
) )
},
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -1621,20 +1515,15 @@ test(
expect(selectedText.length).toBe(0) expect(selectedText.length).toBe(0)
await expect(u.codeLocator).toHaveText('') await expect(u.codeLocator).toHaveText('')
}) })
await electronApp.close()
} }
) )
test( test(
'Settings persist across restarts', 'Settings persist across restarts',
{ tag: '@electron' }, { tag: '@electron', cleanProjectDir: true },
async ({ browserName }, testInfo) => { async ({ page }, testInfo) => {
await test.step('We can change a user setting like theme', async () => { await test.step('We can change a user setting like theme', async () => {
const { electronApp, page } = await setupElectron({ await page.setBodyDimensions({ width: 1200, height: 500 })
testInfo,
})
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -1645,26 +1534,14 @@ test(
await expect(page.getByTestId('app-theme')).toHaveValue('dark') await expect(page.getByTestId('app-theme')).toHaveValue('dark')
await page.getByTestId('app-theme').selectOption('light') await page.getByTestId('app-theme').selectOption('light')
await electronApp.close()
}) })
await test.step('Starting the app again and we can see the same theme', async () => { await test.step('Starting the app again and we can see the same theme', async () => {
let { electronApp, page } = await setupElectron({ await page.reload()
testInfo, await page.setBodyDimensions({ width: 1200, height: 500 })
cleanProjectDir: false,
})
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await page.getByTestId('user-sidebar-toggle').click()
await page.getByTestId('user-settings').click()
await expect(page.getByTestId('app-theme')).toHaveValue('light') await expect(page.getByTestId('app-theme')).toHaveValue('light')
await electronApp.close()
}) })
} }
) )
@ -1673,11 +1550,8 @@ test(
test.fixme( test.fixme(
'Original project name persist after onboarding', 'Original project name persist after onboarding',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await page.setBodyDimensions({ width: 1200, height: 500 })
testInfo,
})
await page.setViewportSize({ width: 1200, height: 500 })
const getAllProjects = () => page.getByTestId('project-link').all() const getAllProjects = () => page.getByTestId('project-link').all()
page.on('console', console.log) page.on('console', console.log)
@ -1709,7 +1583,5 @@ test.fixme(
await expect(projectLink).toContainText(projectNames[index]) await expect(projectLink).toContainText(projectNames[index])
} }
}) })
await electronApp.close()
} }
) )

View File

@ -1,36 +1,26 @@
import { test, expect, Page } from '@playwright/test' import { test, expect, Page } from './zoo-test'
import { join } from 'path' import path from 'path'
import * as fsp from 'fs/promises' import * as fsp from 'fs/promises'
import { import { getUtils, executorInputPath } from './test-utils'
getUtils,
setup,
setupElectron,
tearDown,
executorInputPath,
} from './test-utils'
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates' import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Regression tests', () => { test.describe('Regression tests', () => {
// bugs we found that don't fit neatly into other categories // bugs we found that don't fit neatly into other categories
test('bad model has inline error #3251', async ({ page }) => { test('bad model has inline error #3251', async ({
context,
page,
homePage,
}) => {
// because the model has `line([0,0]..` it is valid code, but the model is invalid // because the model has `line([0,0]..` it is valid code, but the model is invalid
// regression test for https://github.com/KittyCAD/modeling-app/issues/3251 // regression test for https://github.com/KittyCAD/modeling-app/issues/3251
// Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics // Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await context.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch2 = startSketchOn("XY") `sketch2 = startSketchOn("XY")
sketch001 = startSketchAt([-0, -0]) sketch001 = startSketchAt([-0, -0])
|> line([0, 0], %) |> line([0, 0], %)
|> line([-4.84, -5.29], %) |> line([-4.84, -5.29], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
@ -38,9 +28,10 @@ sketch001 = startSketchAt([-0, -0])
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.waitForPageLoad()
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
@ -56,6 +47,7 @@ sketch001 = startSketchAt([-0, -0])
}) })
test('user should not have to press down twice in cmdbar', async ({ test('user should not have to press down twice in cmdbar', async ({
page, page,
homePage,
}) => { }) => {
// because the model has `line([0,0]..` it is valid code, but the model is invalid // because the model has `line([0,0]..` it is valid code, but the model is invalid
// regression test for https://github.com/KittyCAD/modeling-app/issues/3251 // regression test for https://github.com/KittyCAD/modeling-app/issues/3251
@ -64,26 +56,38 @@ sketch001 = startSketchAt([-0, -0])
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch2 = startSketchOn("XY") `sketch001 = startSketchOn('XY')
sketch001 = startSketchAt([-0, -0]) |> startProfileAt([82.33, 238.21], %)
|> line([0, 0], %) |> angledLine([0, 288.63], %, $rectangleSegmentA001)
|> line([-4.84, -5.29], %) |> angledLine([
segAng(rectangleSegmentA001) - 90,
197.97
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)` |> close(%)
extrude001 = extrude(50, sketch001)
`
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await page.goto('/') await homePage.goToModelingScene()
await u.waitForPageLoad() await u.waitForPageLoad()
await test.step('Check arrow down works', async () => { await test.step('Check arrow down works', async () => {
await page.getByTestId('command-bar-open-button').hover()
await page.getByTestId('command-bar-open-button').click() await page.getByTestId('command-bar-open-button').click()
await page const floppy = page.getByRole('option', {
.getByRole('option', { name: 'floppy disk arrow Export' }) name: 'floppy disk arrow Export',
.click() })
await floppy.click()
// press arrow down key twice // press arrow down key twice
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
@ -115,7 +119,7 @@ sketch001 = startSketchAt([-0, -0])
) )
}) })
}) })
test('executes on load', async ({ page }) => { test('executes on load', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -127,9 +131,10 @@ sketch001 = startSketchAt([-0, -0])
|> line([-23.44, 0.52], %)` |> line([-23.44, 0.52], %)`
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.waitForPageLoad()
// expand variables section // expand variables section
const variablesTabButton = page.getByTestId('variables-pane-button') const variablesTabButton = page.getByTestId('variables-pane-button')
@ -148,14 +153,15 @@ sketch001 = startSketchAt([-0, -0])
).toBeVisible() ).toBeVisible()
}) })
test('re-executes', async ({ page }) => { test('re-executes', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem('persistCode', `myVar = 5`) localStorage.setItem('persistCode', `myVar = 5`)
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.waitForPageLoad()
const variablesTabButton = page.getByTestId('variables-pane-button') const variablesTabButton = page.getByTestId('variables-pane-button')
await variablesTabButton.click() await variablesTabButton.click()
@ -174,7 +180,7 @@ sketch001 = startSketchAt([-0, -0])
page.locator('.pretty-json-container >> text=myVar:67') page.locator('.pretty-json-container >> text=myVar:67')
).toBeVisible() ).toBeVisible()
}) })
test('ProgramMemory can be serialised', async ({ page }) => { test('ProgramMemory can be serialised', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -193,13 +199,14 @@ sketch001 = startSketchAt([-0, -0])
}, %)` }, %)`
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
const messages: string[] = [] const messages: string[] = []
// Listen for all console events and push the message text to an array // Listen for all console events and push the message text to an array
page.on('console', (message) => messages.push(message.text())) page.on('console', (message) => messages.push(message.text()))
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.waitForPageLoad()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -212,19 +219,26 @@ sketch001 = startSketchAt([-0, -0])
}) })
}) })
}) })
test('ensure the Zoo logo is not a link in browser app', async ({ page }) => {
// Not relevant to us anymore, or at least for the time being.
test.skip('ensure the Zoo logo is not a link in browser app', async ({
page,
homePage,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.waitForPageLoad()
const zooLogo = page.locator('[data-testid="app-logo"]') const zooLogo = page.locator('[data-testid="app-logo"]')
// Make sure it's not a link // Make sure it's not a link
await expect(zooLogo).not.toHaveAttribute('href') await expect(zooLogo).not.toHaveAttribute('href')
}) })
test( test(
'Position _ Is Out Of Range... regression test', 'Position _ Is Out Of Range... regression test',
{ tag: ['@skipWin'] }, { tag: ['@skipWin'] },
async ({ page }) => { async ({ context, page, homePage }) => {
// SKip on windows, its being weird. // SKip on windows, its being weird.
test.skip( test.skip(
process.platform === 'win32', process.platform === 'win32',
@ -233,8 +247,8 @@ sketch001 = startSketchAt([-0, -0])
const u = await getUtils(page) const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await page.addInitScript(async () => { await context.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`exampleSketch = startSketchOn("XZ") `exampleSketch = startSketchOn("XZ")
@ -250,8 +264,9 @@ sketch001 = startSketchAt([-0, -0])
}) })
await expect(async () => { await expect(async () => {
await page.goto('/') await homePage.goToModelingScene()
await u.waitForPageLoad() await u.waitForPageLoad()
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible({ await expect(page.locator('.cm-lint-marker-error')).toBeVisible({
timeout: 1_000, timeout: 1_000,
@ -306,6 +321,7 @@ sketch001 = startSketchAt([-0, -0])
test('when engine fails export we handle the failure and alert the user', async ({ test('when engine fails export we handle the failure and alert the user', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript( await page.addInitScript(
@ -316,9 +332,10 @@ sketch001 = startSketchAt([-0, -0])
{ code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } { code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR }
) )
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.waitForPageLoad()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -374,7 +391,6 @@ sketch001 = startSketchAt([-0, -0])
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
await u.clearCommandLogs()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
@ -408,7 +424,7 @@ sketch001 = startSketchAt([-0, -0])
test( test(
'ensure you can not export while an export is already going', 'ensure you can not export while an export is already going',
{ tag: ['@skipLinux', '@skipWin'] }, { tag: ['@skipLinux', '@skipWin'] },
async ({ page }) => { async ({ page, homePage }) => {
// This is being weird on ubuntu and windows. // This is being weird on ubuntu and windows.
test.skip( test.skip(
// eslint-disable-next-line jest/valid-title // eslint-disable-next-line jest/valid-title
@ -428,9 +444,10 @@ sketch001 = startSketchAt([-0, -0])
} }
) )
await page.setViewportSize({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.waitForPageLoad()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -500,20 +517,17 @@ sketch001 = startSketchAt([-0, -0])
test( test(
`Network health indicator only appears in modeling view`, `Network health indicator only appears in modeling view`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName: _ }, testInfo) => { async ({ context, page }, testInfo) => {
const { electronApp, page } = await setupElectron({ await context.folderSetupFn(async (dir) => {
testInfo, const bracketDir = path.join(dir, 'bracket')
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile( await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'), executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl') path.join(bracketDir, 'main.kcl')
) )
},
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
const u = await getUtils(page) const u = await getUtils(page)
// Locators // Locators
@ -539,18 +553,17 @@ sketch001 = startSketchAt([-0, -0])
await u.waitForPageLoad() await u.waitForPageLoad()
await expect(networkHealthIndicator).toContainText('Connected') await expect(networkHealthIndicator).toContainText('Connected')
}) })
await electronApp.close()
} }
) )
test(`View gizmo stays visible even when zoomed out all the way`, async ({ test(`View gizmo stays visible even when zoomed out all the way`, async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
// Constants and locators // Constants and locators
const planeColor: [number, number, number] = [170, 220, 170] const planeColor: [number, number, number] = [161, 220, 155]
const bgColor: [number, number, number] = [27, 27, 27] const bgColor: [number, number, number] = [27, 27, 27]
const middlePixelIsColor = async (color: [number, number, number]) => { const middlePixelIsColor = async (color: [number, number, number]) => {
return u.getGreatestPixDiff({ x: 600, y: 250 }, color) return u.getGreatestPixDiff({ x: 600, y: 250 }, color)
@ -561,8 +574,9 @@ sketch001 = startSketchAt([-0, -0])
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem('persistCode', '') localStorage.setItem('persistCode', '')
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.closeKclCodePanel() await u.closeKclCodePanel()
}) })

View File

@ -7,6 +7,8 @@ try {
.split('\n') .split('\n')
.filter((line) => line && line.length > 1) .filter((line) => line && line.length > 1)
.forEach((line) => { .forEach((line) => {
// Allow line comments.
if (line.trimStart().startsWith('#')) return
const [key, value] = line.split('=') const [key, value] = line.split('=')
// prefer env vars over secrets file // prefer env vars over secrets file
secrets[key] = process.env[key] || (value as any).replaceAll('"', '') secrets[key] = process.env[key] || (value as any).replaceAll('"', '')

View File

@ -1,27 +1,20 @@
import { test, expect, Page } from '@playwright/test' import { test, expect, Page } from './zoo-test'
import { test as test2, expect as expect2 } from './fixtures/fixtureSetup' import fs from 'node:fs/promises'
import path from 'node:path'
import { HomePageFixture } from './fixtures/homePageFixture'
import { import {
getMovementUtils, getMovementUtils,
getUtils, getUtils,
PERSIST_MODELING_CONTEXT, PERSIST_MODELING_CONTEXT,
setup,
tearDown,
} from './test-utils' } from './test-utils'
import { uuidv4, roundOff } from 'lib/utils' import { uuidv4, roundOff } from 'lib/utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Sketch tests', () => { test.describe('Sketch tests', () => {
test('multi-sketch file shows multiple Edit Sketch buttons', async ({ test('multi-sketch file shows multiple Edit Sketch buttons', async ({
page, page,
context, context,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
const selectionsSnippets = { const selectionsSnippets = {
@ -79,9 +72,9 @@ test.describe('Sketch tests', () => {
}, },
selectionsSnippets selectionsSnippets
) )
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -108,6 +101,7 @@ test.describe('Sketch tests', () => {
}) })
test('Can delete most of a sketch and the line tool will still work', async ({ test('Can delete most of a sketch and the line tool will still work', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -120,12 +114,9 @@ test.describe('Sketch tests', () => {
) )
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await expect(async () => { await expect(async () => {
await page.mouse.click(700, 200)
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click() await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
await expect( await expect(
page.getByRole('button', { name: 'Edit Sketch' }) page.getByRole('button', { name: 'Edit Sketch' })
@ -142,8 +133,7 @@ test.describe('Sketch tests', () => {
await page.keyboard.press('Home') await page.keyboard.press('Home')
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
await u.openAndClearDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -151,26 +141,31 @@ test.describe('Sketch tests', () => {
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect(async () => { await expect(async () => {
await page.mouse.move(700, 200, { steps: 25 })
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await expect.poll(u.normalisedEditorCode, { timeout: 1000 }) await expect
.toBe(`sketch001 = startSketchOn('XZ') .poll(u.crushKclCodeIntoOneLineAndThenMaybeSome, { timeout: 1000 })
|> startProfileAt([12.34, -12.34], %) .toBe(
|> yLine(12.34, %) `sketch001 = startSketchOn('XZ')
|> startProfileAt([4.61,-14.01], %)
`) |> yLine(15.95, %)
`
.replaceAll(' ', '')
.replaceAll('\n', '')
)
}).toPass({ timeout: 40_000, intervals: [1_000] }) }).toPass({ timeout: 40_000, intervals: [1_000] })
}) })
test('Can exit selection of face', async ({ page }) => {
test('Can exit selection of face', async ({ page, homePage }) => {
// Load the app with the code panes // Load the app with the code panes
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem('persistCode', ``) localStorage.setItem('persistCode', ``)
}) })
const u = await getUtils(page) await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect( await expect(
@ -187,6 +182,7 @@ test.describe('Sketch tests', () => {
test.describe('Can edit segments by dragging their handles', () => { test.describe('Can edit segments by dragging their handles', () => {
const doEditSegmentsByDraggingHandle = async ( const doEditSegmentsByDraggingHandle = async (
page: Page, page: Page,
homePage: HomePageFixture,
openPanes: string[] openPanes: string[]
) => { ) => {
// Load the app with the code panes // Load the app with the code panes
@ -202,9 +198,8 @@ test.describe('Sketch tests', () => {
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -318,7 +313,7 @@ test.describe('Sketch tests', () => {
|> line([1.97, 2.06], %) |> line([1.97, 2.06], %)
|> close(%)`) |> close(%)`)
} }
test('code pane open at start-handles', async ({ page }) => { test('code pane open at start-handles', async ({ page, homePage }) => {
// Load the app with the code panes // Load the app with the code panes
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -331,10 +326,10 @@ test.describe('Sketch tests', () => {
}) })
) )
}) })
await doEditSegmentsByDraggingHandle(page, ['code']) await doEditSegmentsByDraggingHandle(page, homePage, ['code'])
}) })
test('code pane closed at start-handles', async ({ page }) => { test('code pane closed at start-handles', async ({ page, homePage }) => {
// Load the app with the code panes // Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => { await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem( localStorage.setItem(
@ -342,12 +337,14 @@ test.describe('Sketch tests', () => {
JSON.stringify({ openPanes: [] }) JSON.stringify({ openPanes: [] })
) )
}, PERSIST_MODELING_CONTEXT) }, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(page, []) await doEditSegmentsByDraggingHandle(page, homePage, [])
}) })
}) })
test('Can edit a circle center and radius by dragging its handles', async ({ test('Can edit a circle center and radius by dragging its handles', async ({
page, page,
editor,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -358,9 +355,8 @@ test.describe('Sketch tests', () => {
) )
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -399,6 +395,7 @@ test.describe('Sketch tests', () => {
).toBeVisible() ).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400) await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText() let prevContent = await page.locator('.cm-content').innerText()
await expect(page.getByTestId('segment-overlay')).toHaveCount(1) await expect(page.getByTestId('segment-overlay')).toHaveCount(1)
@ -409,7 +406,9 @@ test.describe('Sketch tests', () => {
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] - dragPX }, targetPosition: { x: startPX[0] + dragPX, y: startPX[1] - dragPX },
}) })
await page.waitForTimeout(100) await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
await editor.expectEditor.not.toContain(prevContent)
prevContent = await page.locator('.cm-content').innerText() prevContent = await page.locator('.cm-content').innerText()
}) })
@ -422,18 +421,20 @@ test.describe('Sketch tests', () => {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y }, sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX * 2, y: lineEnd.y + dragPX }, targetPosition: { x: lineEnd.x + dragPX * 2, y: lineEnd.y + dragPX },
}) })
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await editor.expectEditor.not.toContain(prevContent)
prevContent = await page.locator('.cm-content').innerText() prevContent = await page.locator('.cm-content').innerText()
}) })
// expect the code to have changed // expect the code to have changed
await expect(page.locator('.cm-content')) await editor.expectEditor.toContain(
.toHaveText(`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> circle({ center = [7.26, -2.37], radius = 11.44 }, %) |> circle({ center = [7.26, -2.37], radius = 11.44 }, %)`,
`) { shouldNormalise: true }
)
}) })
test('Can edit a sketch that has been extruded in the same pipe', async ({ test('Can edit a sketch that has been extruded in the same pipe', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -448,9 +449,8 @@ test.describe('Sketch tests', () => {
) )
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -504,11 +504,11 @@ test.describe('Sketch tests', () => {
await page.waitForTimeout(100) await page.waitForTimeout(100)
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]') const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.waitForTimeout(100)
await page.dragAndDrop('#stream', '#stream', { await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y }, sourcePosition: { x: lineEnd.x - 15, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX }, targetPosition: { x: lineEnd.x, y: lineEnd.y + 15 },
}) })
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText() prevContent = await page.locator('.cm-content').innerText()
@ -517,8 +517,8 @@ test.describe('Sketch tests', () => {
await page.dragAndDrop('#stream', '#stream', { await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: tangentEnd.x + 10, y: tangentEnd.y - 5 }, sourcePosition: { x: tangentEnd.x + 10, y: tangentEnd.y - 5 },
targetPosition: { targetPosition: {
x: tangentEnd.x + dragPX, x: tangentEnd.x,
y: tangentEnd.y + dragPX, y: tangentEnd.y - 15,
}, },
}) })
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -528,15 +528,16 @@ test.describe('Sketch tests', () => {
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ') .toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([7.12, -12.68], %) |> startProfileAt([7.12, -12.68], %)
|> line([15.39, -2.78], %) |> line([12.68, -1.09], %)
|> tangentialArcTo([27.6, -3.05], %) |> tangentialArcTo([24.89, 0.68], %)
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
`) `)
}) })
test('Can edit a sketch that has been revolved in the same pipe', async ({ test('Can edit a sketch that has been revolved in the same pipe', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -551,9 +552,8 @@ test.describe('Sketch tests', () => {
) )
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await homePage.goToModelingScene()
await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -636,12 +636,13 @@ test.describe('Sketch tests', () => {
|> close(%) |> close(%)
|> revolve({ axis = "X" }, %)`) |> revolve({ axis = "X" }, %)`)
}) })
test('Can add multiple sketches', async ({ page }) => { test('Can add multiple sketches', async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize)
await u.waitForAuthSkipAppStart() const viewportSize = { width: 1200, height: 500 }
await page.setBodyDimensions(viewportSize)
await homePage.goToModelingScene()
await u.openDebugPanel() await u.openDebugPanel()
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 } const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
@ -736,9 +737,8 @@ test.describe('Sketch tests', () => {
scale = 1 scale = 1
) => { ) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
const code = `sketch001 = startSketchOn('-XZ') const code = `sketch001 = startSketchOn('-XZ')
@ -820,16 +820,19 @@ test.describe('Sketch tests', () => {
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.removeCurrentCode() await u.removeCurrentCode()
} }
test('[0, 100, 100]', async ({ page }) => { test('[0, 100, 100]', async ({ page, homePage }) => {
await homePage.goToModelingScene()
await doSnapAtDifferentScales(page, [0, 100, 100], 0.01) await doSnapAtDifferentScales(page, [0, 100, 100], 0.01)
}) })
test('[0, 10000, 10000]', async ({ page }) => { test('[0, 10000, 10000]', async ({ page, homePage }) => {
await homePage.goToModelingScene()
await doSnapAtDifferentScales(page, [0, 10000, 10000]) await doSnapAtDifferentScales(page, [0, 10000, 10000])
}) })
}) })
test('exiting a close extrude, has the extrude button enabled ready to go', async ({ test('exiting a close extrude, has the extrude button enabled ready to go', async ({
page, page,
homePage,
}) => { }) => {
// this was a regression https://github.com/KittyCAD/modeling-app/issues/2832 // this was a regression https://github.com/KittyCAD/modeling-app/issues/2832
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -841,15 +844,15 @@ test.describe('Sketch tests', () => {
|> line([1.02, -1.32], %, $seg01) |> line([1.02, -1.32], %, $seg01)
|> line([-1.01, -0.77], %) |> line([-1.01, -0.77], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
` `
) )
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// wait for execution done // wait for execution done
await u.openDebugPanel() await u.openDebugPanel()
@ -885,7 +888,10 @@ test.describe('Sketch tests', () => {
timeout: 10_000, timeout: 10_000,
}) })
}) })
test("Existing sketch with bad code delete user's code", async ({ page }) => { test("Existing sketch with bad code delete user's code", async ({
page,
homePage,
}) => {
// this was a regression https://github.com/KittyCAD/modeling-app/issues/2832 // this was a regression https://github.com/KittyCAD/modeling-app/issues/2832
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -897,15 +903,15 @@ test.describe('Sketch tests', () => {
|> line([-1.01, -0.77], %) |> line([-1.01, -0.77], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(5, sketch001) extrude001 = extrude(5, sketch001)
` `
) )
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -937,30 +943,18 @@ extrude001 = extrude(5, sketch001)
|> line([-1.01, -0.77], %) |> line([-1.01, -0.77], %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
extrude001 = extrude(5, sketch001) extrude001 = extrude(5, sketch001)
sketch002 = startSketchOn(extrude001, 'END') sketch002 = startSketchOn(extrude001, 'END')
|> |>
`.replace(/\s/g, '') `.replace(/\s/g, '')
) )
}) })
test('empty-scene default-planes act as expected', async ({
/* TODO: once we fix bug turn on.
test('empty-scene default-planes act as expected when spaces in file', async ({
page, page,
browserName, browserName,
}) => { }) => {
test.skip(
browserName === 'webkit',
'Skip on Safari until `window.tearDown` is working there'
)
/**
* Tests the following things
* 1) The the planes are there on load because the scene is empty
* 2) The planes don't changes color when hovered initially
* 3) Putting something in the scene makes the planes hidden
* 4) Removing everything from the scene shows the plans again
* 3) Once "start sketch" is click, the planes do respond to hovers
* 4) Selecting a plan works as expected, i.e. sketch mode
* 5) Reloading the scene with something already in the scene means the planes are hidden
*/
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -987,12 +981,122 @@ sketch002 = startSketchOn(extrude001, 'END')
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
// Fill with spaces
await u.codeLocator.fill(`
`)
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
})
test('empty-scene default-planes act as expected when only code comments in file', async ({
page,
browserName,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const XYPlanePoint = { x: 774, y: 116 } as const
const unHoveredColor: [number, number, number] = [47, 47, 93]
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await u.openAndClearDebugPanel()
// Fill with spaces
await u.codeLocator.fill(`// this is a code comments ya nerds
`)
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
})*/
test('empty-scene default-planes act as expected', async ({
page,
homePage,
}) => {
/**
* Tests the following things
* 1) The the planes are there on load because the scene is empty
* 2) The planes don't changes color when hovered initially
* 3) Putting something in the scene makes the planes hidden
* 4) Removing everything from the scene shows the plans again
* 3) Once "start sketch" is click, the planes do respond to hovers
* 4) Selecting a plan works as expected, i.e. sketch mode
* 5) Reloading the scene with something already in the scene means the planes are hidden
*/
const u = await getUtils(page)
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const XYPlanePoint = { x: 774, y: 116 } as const
const unHoveredColor: [number, number, number] = [47, 47, 93]
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await u.openAndClearDebugPanel()
await u.codeLocator.fill(`sketch001 = startSketchOn('XY') await u.codeLocator.fill(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %) |> startProfileAt([-10, -10], %)
|> line([20, 0], %) |> line([20, 0], %)
|> line([0, 20], %) |> line([0, 20], %)
|> xLine(-20, %) |> xLine(-20, %)
`) `)
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -1013,7 +1117,7 @@ sketch002 = startSketchOn(extrude001, 'END')
// click start Sketch // click start Sketch
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y, { steps: 5 }) await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y, { steps: 50 })
const hoveredColor: [number, number, number] = [93, 93, 127] const hoveredColor: [number, number, number] = [93, 93, 127]
// now that we're expecting the user to select a plan, it does respond to hover // now that we're expecting the user to select a plan, it does respond to hover
await expect await expect
@ -1029,7 +1133,7 @@ sketch002 = startSketchOn(extrude001, 'END')
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ') await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt([11.8, 9.09], %) |> startProfileAt([11.8, 9.09], %)
|> line([3.39, -3.39], %) |> line([3.39, -3.39], %)
`) `)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -1037,11 +1141,9 @@ sketch002 = startSketchOn(extrude001, 'END')
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([11.8, 9.09], %) |> startProfileAt([11.8, 9.09], %)
|> line([3.39, -3.39], %) |> line([3.39, -3.39], %)
` `
) )
}) })
await page.reload()
await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -1053,16 +1155,9 @@ sketch002 = startSketchOn(extrude001, 'END')
).toBeLessThan(3) ).toBeLessThan(3)
}) })
test('Can attempt to sketch on revolved face', async ({ test('Can attempt to sketch on revolved face', async ({ page, homePage }) => {
page,
browserName,
}) => {
test.skip(
browserName === 'webkit',
'Skip on Safari until `window.tearDown` is working there'
)
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -1087,7 +1182,7 @@ sketch002 = startSketchOn(extrude001, 'END')
) )
}) })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -1119,9 +1214,10 @@ sketch002 = startSketchOn(extrude001, 'END')
test('Can sketch on face when user defined function was used in the sketch', async ({ test('Can sketch on face when user defined function was used in the sketch', async ({
page, page,
homePage,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
// Checking for a regression that performs a sketch when a user defined function // Checking for a regression that performs a sketch when a user defined function
// is declared at the top of the file and used in the sketch that is being drawn on. // is declared at the top of the file and used in the sketch that is being drawn on.
@ -1132,16 +1228,16 @@ sketch002 = startSketchOn(extrude001, 'END')
'persistCode', 'persistCode',
`fn in2mm = (inches) => { `fn in2mm = (inches) => {
return inches * 25.4 return inches * 25.4
} }
const railTop = in2mm(.748) const railTop = in2mm(.748)
const railSide = in2mm(.024) const railSide = in2mm(.024)
const railBaseWidth = in2mm(.612) const railBaseWidth = in2mm(.612)
const railWideWidth = in2mm(.835) const railWideWidth = in2mm(.835)
const railBaseLength = in2mm(.200) const railBaseLength = in2mm(.200)
const railClampable = in2mm(.200) const railClampable = in2mm(.200)
const rail = startSketchOn('XZ') const rail = startSketchOn('XZ')
|> startProfileAt([ |> startProfileAt([
-railTop / 2, -railTop / 2,
railClampable + railBaseLength railClampable + railBaseLength
@ -1175,7 +1271,7 @@ const rail = startSketchOn('XZ')
const center = { x: 600, y: 250 } const center = { x: 600, y: 250 }
const rectangleSize = 20 const rectangleSize = 20
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
// Start a sketch // Start a sketch
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
@ -1214,27 +1310,33 @@ const rail = startSketchOn('XZ')
}) })
}) })
test2.describe('Sketch mode should be toleratant to syntax errors', () => { test.describe('Sketch mode should be toleratant to syntax errors', () => {
test2( test(
'adding a syntax error, recovers after fixing', 'adding a syntax error, recovers after fixing',
{ tag: ['@skipWin'] }, { tag: ['@skipWin'] },
async ({ app, scene, editor, toolbar }) => { async ({ page, homePage, context, scene, editor, toolbar }) => {
test.skip( const file = await fs.readFile(
process.platform === 'win32', path.resolve(
'a codemirror error appears in this test only on windows, that causes the test to fail only because of our "no new error" logic, but it can not be replicated locally' __dirname,
'../../',
'./src/wasm-lib/tests/executor/inputs/e2e-can-sketch-on-chamfer.kcl'
),
'utf-8'
) )
const file = await app.getInputFile('e2e-can-sketch-on-chamfer.kcl') await context.addInitScript((file) => {
await app.initialise(file) localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
const [objClick] = scene.makeMouseHelpers(600, 250) const [objClick] = scene.makeMouseHelpers(600, 250)
const arrowHeadLocation = { x: 604, y: 129 } as const const arrowHeadLocation = { x: 706, y: 129 } as const
const arrowHeadWhite: [number, number, number] = [255, 255, 255] const arrowHeadWhite: [number, number, number] = [255, 255, 255]
const backgroundGray: [number, number, number] = [28, 28, 28] const backgroundGray: [number, number, number] = [28, 28, 28]
const verifyArrowHeadColor = async (c: [number, number, number]) => const verifyArrowHeadColor = async (c: [number, number, number]) =>
scene.expectPixelColor(c, arrowHeadLocation, 15) scene.expectPixelColor(c, arrowHeadLocation, 15)
await test.step('check chamfer selection changes cursor positon', async () => { await test.step('check chamfer selection changes cursor positon', async () => {
await expect2(async () => { await expect(async () => {
// sometimes initial click doesn't register // sometimes initial click doesn't register
await objClick() await objClick()
await editor.expectActiveLinesToBe([ await editor.expectActiveLinesToBe([
@ -1270,24 +1372,36 @@ test2.describe('Sketch mode should be toleratant to syntax errors', () => {
// this checks sketch segments have been drawn // this checks sketch segments have been drawn
await verifyArrowHeadColor(arrowHeadWhite) await verifyArrowHeadColor(arrowHeadWhite)
}) })
await app.page.waitForTimeout(100) await page.waitForTimeout(100)
} }
) )
}) })
test2.describe(`Sketching with offset planes`, () => { test.describe(`Sketching with offset planes`, () => {
test2( test(`Can select an offset plane to sketch on`, async ({
`Can select an offset plane to sketch on`, context,
async ({ app, scene, toolbar, editor }) => { page,
scene,
toolbar,
editor,
homePage,
}) => {
// We seed the scene with a single offset plane // We seed the scene with a single offset plane
await app.initialise(`offsetPlane001 = offsetPlane("XY", 10)`) await context.addInitScript(() => {
localStorage.setItem(
'persistCode',
`offsetPlane001 = offsetPlane("XY", 10)`
)
})
await homePage.goToModelingScene()
const [planeClick, planeHover] = scene.makeMouseHelpers(650, 200) const [planeClick, planeHover] = scene.makeMouseHelpers(650, 200)
await test2.step(`Start sketching on the offset plane`, async () => { await test.step(`Start sketching on the offset plane`, async () => {
await toolbar.startSketchPlaneSelection() await toolbar.startSketchPlaneSelection()
await test2.step(`Hovering should highlight code`, async () => { await test.step(`Hovering should highlight code`, async () => {
await planeHover() await planeHover()
await editor.expectState({ await editor.expectState({
activeLines: [`offsetPlane001=offsetPlane("XY",10)`], activeLines: [`offsetPlane001=offsetPlane("XY",10)`],
@ -1296,22 +1410,18 @@ test2.describe(`Sketching with offset planes`, () => {
}) })
}) })
await test2.step( await test.step(`Clicking should select the plane and enter sketch mode`, async () => {
`Clicking should select the plane and enter sketch mode`,
async () => {
await planeClick() await planeClick()
// Have to wait for engine-side animation to finish // Have to wait for engine-side animation to finish
await app.page.waitForTimeout(600) await page.waitForTimeout(600)
await expect2(toolbar.lineBtn).toBeEnabled() await expect(toolbar.lineBtn).toBeEnabled()
await editor.expectEditor.toContain('startSketchOn(offsetPlane001)') await editor.expectEditor.toContain('startSketchOn(offsetPlane001)')
await editor.expectState({ await editor.expectState({
activeLines: [`offsetPlane001=offsetPlane("XY",10)`], activeLines: [`offsetPlane001=offsetPlane("XY",10)`],
diagnostics: [], diagnostics: [],
highlightedCode: '', highlightedCode: '',
}) })
}
)
}) })
} })
) })
}) })

View File

@ -47,7 +47,12 @@ test.beforeEach(async ({ page }) => {
test.setTimeout(60_000) test.setTimeout(60_000)
test(
// We test this end to end already - getting this to work on web just to take
// a snapshot of it feels weird. I'd rather our regular tests fail.
// The primary failure is doExport now relies on the filesystem. We can follow
// up with another PR if we want this back.
test.skip(
'exports of each format should work', 'exports of each format should work',
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] }, { tag: ['@snapshot', '@skipWin', '@skipMacos'] },
async ({ page, context }) => { async ({ page, context }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 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: 58 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 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: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 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: 39 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 40 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: 42 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -109,242 +109,21 @@ keychain = startSketchOn("XY")
|> close(%) |> close(%)
|> extrude(thickness, %) |> extrude(thickness, %)
// generated from /home/paultag/Downloads/zma-logomark.svg keychain1 = startSketchOn("XY")
fn svg = (surface, origin, depth) => { |> startProfileAt([0, 0], %)
let a0 = surface |> startProfileAt([origin[0] + 45.430427, origin[1] + -14.627736], %) |> lineTo([width, 0], %)
|> bezierCurve({ |> lineTo([width, height], %)
control1: [ 0, 0.764157 ], |> lineTo([0, height], %)
control2: [ 0, 1.528314 ],
to: [ 0, 2.292469 ]
}, %)
|> bezierCurve({
control1: [ -3.03202, 0 ],
control2: [ -6.064039, 0 ],
to: [ -9.09606, 0 ]
}, %)
|> bezierCurve({
control1: [ 0, -1.077657 ],
control2: [ 0, -2.155312 ],
to: [ 0, -3.232969 ]
}, %)
|> bezierCurve({
control1: [ 2.741805, 0 ],
control2: [ 5.483613, 0 ],
to: [ 8.225417, 0 ]
}, %)
|> bezierCurve({
control1: [ -2.740682, -2.961815 ],
control2: [ -5.490342, -5.925794 ],
to: [ -8.225417, -8.886255 ]
}, %)
|> bezierCurve({
control1: [ 0, -0.723995 ],
control2: [ 0, -1.447988 ],
to: [ 0, -2.171981 ]
}, %)
|> bezierCurve({
control1: [ 0.712124, 0.05061 ],
control2: [ 1.511636, -0.09877 ],
to: [ 2.172096, 0.07005 ]
}, %)
|> bezierCurve({
control1: [ 0.68573, 0.740811 ],
control2: [ 1.371459, 1.481622 ],
to: [ 2.057187, 2.222436 ]
}, %)
|> bezierCurve({
control1: [ 0, -0.76416 ],
control2: [ 0, -1.52832 ],
to: [ 0, -2.29248 ]
}, %)
|> bezierCurve({
control1: [ 3.032013, 0 ],
control2: [ 6.064026, 0 ],
to: [ 9.096038, 0 ]
}, %)
|> bezierCurve({
control1: [ 0, 1.077657 ],
control2: [ 0, 2.155314 ],
to: [ 0, 3.232973 ]
}, %)
|> bezierCurve({
control1: [ -2.741312, 0 ],
control2: [ -5.482623, 0 ],
to: [ -8.223936, 0 ]
}, %)
|> bezierCurve({
control1: [ 2.741313, 2.961108 ],
control2: [ 5.482624, 5.922216 ],
to: [ 8.223936, 8.883325 ]
}, %)
|> bezierCurve({
control1: [ 0, 0.724968 ],
control2: [ 0, 1.449938 ],
to: [ 0, 2.174907 ]
}, %)
|> bezierCurve({
control1: [ -0.712656, -0.05145 ],
control2: [ -1.512554, 0.09643 ],
to: [ -2.173592, -0.07298 ]
}, %)
|> bezierCurve({
control1: [ -0.685222, -0.739834 ],
control2: [ -1.370445, -1.479669 ],
to: [ -2.055669, -2.219505 ]
}, %)
|> close(%) |> close(%)
|> extrude(depth, %) |> extrude(thickness, %)
let a1 = surface |> startProfileAt([origin[0] + 57.920488, origin[1] + -15.244943], %) keychain2 = startSketchOn("XY")
|> bezierCurve({ |> startProfileAt([0, 0], %)
control1: [ -2.78904, 0.106635 ], |> lineTo([width, 0], %)
control2: [ -5.052548, -2.969529 ], |> lineTo([width, height], %)
to: [ -4.055141, -5.598369 ] |> lineTo([0, height], %)
}, %)
|> bezierCurve({
control1: [ 0.841523, -0.918736 ],
control2: [ 0.439412, -1.541892 ],
to: [ -0.368488, -2.214378 ]
}, %)
|> bezierCurve({
control1: [ -0.418245, -0.448461 ],
control2: [ -0.836489, -0.896922 ],
to: [ -1.254732, -1.345384 ]
}, %)
|> bezierCurve({
control1: [ -2.76806, 2.995359 ],
control2: [ -2.32667, 8.18409 ],
to: [ 0.897655, 10.678932 ]
}, %)
|> bezierCurve({
control1: [ 2.562822, 2.186098 ],
control2: [ 6.605111, 2.28043 ],
to: [ 9.271202, 0.226476 ]
}, %)
|> bezierCurve({
control1: [ -0.743744, -0.797465 ],
control2: [ -1.487487, -1.594932 ],
to: [ -2.231232, -2.392397 ]
}, %)
|> bezierCurve({
control1: [ -0.672938, 0.421422 ],
control2: [ -1.465362, 0.646946 ],
to: [ -2.259264, 0.64512 ]
}, %)
|> close(%) |> close(%)
|> extrude(depth, %) |> extrude(thickness, %)
let a2 = surface |> startProfileAt([origin[0] + 62.19406300000001, origin[1] + -19.500698999999997], %)
|> bezierCurve({
control1: [ 0.302938, 1.281141 ],
control2: [ -1.53575, 2.434288 ],
to: [ -0.10908, 3.279477 ]
}, %)
|> bezierCurve({
control1: [ 0.504637, 0.54145 ],
control2: [ 1.009273, 1.082899 ],
to: [ 1.513909, 1.624348 ]
}, %)
|> bezierCurve({
control1: [ 2.767778, -2.995425 ],
control2: [ 2.327135, -8.184384 ],
to: [ -0.897661, -10.679047 ]
}, %)
|> bezierCurve({
control1: [ -2.562947, -2.186022 ],
control2: [ -6.604089, -2.279606 ],
to: [ -9.271196, -0.227813 ]
}, %)
|> bezierCurve({
control1: [ 0.744231, 0.797952 ],
control2: [ 1.488461, 1.595904 ],
to: [ 2.232692, 2.393856 ]
}, %)
|> bezierCurve({
control1: [ 2.302377, -1.564629 ],
control2: [ 5.793126, -0.15358 ],
to: [ 6.396577, 2.547372 ]
}, %)
|> bezierCurve({
control1: [ 0.08981, 0.346302 ],
control2: [ 0.134865, 0.704078 ],
to: [ 0.13476, 1.061807 ]
}, %)
|> close(%)
|> extrude(depth, %)
let a3 = surface |> startProfileAt([origin[0] + 74.124866, origin[1] + -15.244943], %)
|> bezierCurve({
control1: [ -2.78904, 0.106635 ],
control2: [ -5.052549, -2.969529 ],
to: [ -4.055142, -5.598369 ]
}, %)
|> bezierCurve({
control1: [ 0.841527, -0.918738 ],
control2: [ 0.43941, -1.541892 ],
to: [ -0.368497, -2.214367 ]
}, %)
|> bezierCurve({
control1: [ -0.418254, -0.448466 ],
control2: [ -0.836507, -0.896931 ],
to: [ -1.254761, -1.345395 ]
}, %)
|> bezierCurve({
control1: [ -2.768019, 2.995371 ],
control2: [ -2.326624, 8.184088 ],
to: [ 0.897678, 10.678932 ]
}, %)
|> bezierCurve({
control1: [ 2.56289, 2.186191 ],
control2: [ 6.60516, 2.280307 ],
to: [ 9.271371, 0.226476 ]
}, %)
|> bezierCurve({
control1: [ -0.743808, -0.797465 ],
control2: [ -1.487616, -1.594932 ],
to: [ -2.231424, -2.392397 ]
}, %)
|> bezierCurve({
control1: [ -0.672916, 0.421433 ],
control2: [ -1.465344, 0.646926 ],
to: [ -2.259225, 0.64512 ]
}, %)
|> close(%)
|> extrude(depth, %)
let a4 = surface |> startProfileAt([origin[0] + 77.57333899999998, origin[1] + -16.989262999999998], %)
|> bezierCurve({
control1: [ 0.743298, 0.797463 ],
control2: [ 1.486592, 1.594926 ],
to: [ 2.229888, 2.392389 ]
}, %)
|> bezierCurve({
control1: [ 2.767827, -2.995393 ],
control2: [ 2.327103, -8.184396 ],
to: [ -0.897672, -10.679047 ]
}, %)
|> bezierCurve({
control1: [ -2.562939, -2.186037 ],
control2: [ -6.604077, -2.279589 ],
to: [ -9.271185, -0.227813 ]
}, %)
|> bezierCurve({
control1: [ 0.744243, 0.797952 ],
control2: [ 1.488486, 1.595904 ],
to: [ 2.232729, 2.393856 ]
}, %)
|> bezierCurve({
control1: [ 2.302394, -1.564623 ],
control2: [ 5.793201, -0.153598 ],
to: [ 6.396692, 2.547372 ]
}, %)
|> bezierCurve({
control1: [ 0.32074, 1.215468 ],
control2: [ 0.06159, 2.564765 ],
to: [ -0.690452, 3.573243 ]
}, %)
|> close(%)
|> extrude(depth, %)
box = startSketchOn('XY') box = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
@ -354,7 +133,7 @@ box = startSketchOn('XY')
|> close(%) |> close(%)
|> extrude(10, %) |> extrude(10, %)
sketch001 = startSketchOn(box, revolveAxis) sketch001 = startSketchOn(box, revolveAxis)
|> startProfileAt([5, 10], %) |> startProfileAt([5, 10], %)
|> line([0, -10], %) |> line([0, -10], %)
|> line([2, 0], %) |> line([2, 0], %)
@ -364,18 +143,12 @@ box = startSketchOn('XY')
axis: revolveAxis, axis: revolveAxis,
angle: 90 angle: 90
}, %) }, %)
return 0
}
sketch001 = startSketchOn('XZ')
|> startProfileAt([0.0, 0.0], %)
|> xLine(0.0, %)
|> close(%)
`
svg(startSketchOn(keychain, 'end'), [-33, 32], -thickness)
startSketchOn(keychain, 'end')
|> circle({ center: [
width / 2,
height - (keychainHoleSize + 1.5)
], radius: keychainHoleSize }, %)
|> extrude(-thickness, %)`
export const TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR = `thing = 1` export const TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR = `thing = 1`

View File

@ -1,29 +1,16 @@
import { test, expect } from '@playwright/test' import { test, expect } from './zoo-test'
import { commonPoints, getUtils, setup, tearDown } from './test-utils' import { commonPoints, getUtils } from './test-utils'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Test network and connection issues', () => { test.describe('Test network and connection issues', () => {
test('simulate network down and network little widget', async ({ test('simulate network down and network little widget', async ({
page, page,
browserName, homePage,
}) => { }) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
test.skip(
browserName === 'webkit',
'Skip on Safari until `window.tearDown` is working there'
)
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
const networkToggle = page.getByTestId('network-toggle') const networkToggle = page.getByTestId('network-toggle')
@ -62,7 +49,7 @@ test.describe('Test network and connection issues', () => {
}) })
// Expect the network to be down // Expect the network to be down
await expect(networkToggle).toContainText('Offline') await expect(networkToggle).toContainText('Problem')
// Click the network widget // Click the network widget
await networkWidget.click() await networkWidget.click()
@ -93,26 +80,19 @@ test.describe('Test network and connection issues', () => {
test('Engine disconnect & reconnect in sketch mode', async ({ test('Engine disconnect & reconnect in sketch mode', async ({
page, page,
browserName, homePage,
}) => { }) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit // TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
test.skip(
browserName === 'webkit',
'Skip on Safari until `window.tearDown` is working there'
)
const networkToggle = page.getByTestId('network-toggle') const networkToggle = page.getByTestId('network-toggle')
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await u.waitForAuthSkipAppStart() await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.openDebugPanel() await u.openDebugPanel()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// click on "Start Sketch" button // click on "Start Sketch" button
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
@ -156,7 +136,7 @@ test.describe('Test network and connection issues', () => {
}) })
// Expect the network to be down // Expect the network to be down
await expect(networkToggle).toContainText('Offline') await expect(networkToggle).toContainText('Problem')
// Ensure we are not in sketch mode // Ensure we are not in sketch mode
await expect( await expect(

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