initial playwright setup and test (#1039)
* initial playwright setup and test * try verbose logging * double check resolution * double check token * remove logs * try running on ubuntu and macos * move e2e tests * vitest ignores playwright tests * add a series of tests * tweak yarn setup * typo * update fmt * action typo * rust toolchain? * remove .only from playwright test * A snapshot a day keeps the bugs away! 📷🐛 * A snapshot a day keeps the bugs away! 📷🐛 * add pan and zoom back * try puting os in commit message * A snapshot a day keeps the bugs away! 📷🐛 .os * A snapshot a day keeps the bugs away! 📷🐛 .os * fix commit message * A snapshot a day keeps the bugs away! 📷🐛 (OS: macos-latest) * add command logs to UI for axis tests * typo * fmt * remove .only * add auto complete test * remove .only * update queries to be more playwright recommended * move test utils * remove waits from first test * remove old snapshots * re-arrange files and tests * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * trigger CI * refactor plane test * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * remove .only * try longer wait * try snap shoting * fmt * better CI names * fix * fix linux * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * try make executes on load a bit more robust * fix * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * stabilise snapshots * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: macos-latest) * make tests run linearly * update naming * tidy * .only * update readme * trigger CI * add set tool * util clean up * update readme * readme tweaks * self-review clean up * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * tweak * update readme * Ensure Vite preview server is running before running any Playwright tests (#1114) * Ensure that Vite is serving before tests run * Don't break secrets if developer has extra blank line in env file * @mxschmitt's suggestions * fix * add wait-on types * tsconfig fix * update readme * code template for sktech on each plane test --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Frank Noirot <frank@kittycad.io>
This commit is contained in:
17
.eslintrc
17
.eslintrc
@ -1,4 +1,8 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": [
|
||||
"css-modules"
|
||||
],
|
||||
@ -12,6 +16,15 @@
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
}
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"@typescript-eslint/no-floating-promises": "warn"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
||||
"rules": {
|
||||
"testing-library/prefer-screen-queries": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
114
.github/workflows/playwright.yml
vendored
Normal file
114
.github/workflows/playwright.yml
vendored
Normal file
@ -0,0 +1,114 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
playwright-ubuntu:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache wasm
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
- name: build wasm
|
||||
run: yarn build:wasm
|
||||
- name: build web
|
||||
run: yarn build:local
|
||||
- name: Run ubuntu/chrome snapshots
|
||||
run: yarn playwright test --project="Google Chrome" --update-snapshots e2e/playwright/snapshot-tests.spec.ts
|
||||
env:
|
||||
CI: true
|
||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
- name: check for changes
|
||||
id: git-check
|
||||
run: |
|
||||
git add .
|
||||
if git status | grep -q "Changes to be committed"
|
||||
then
|
||||
echo "::set-output name=modified::true"
|
||||
else
|
||||
echo "::set-output name=modified::false"
|
||||
fi
|
||||
- name: Commit changes, if any
|
||||
if: steps.git-check.outputs.modified == 'true'
|
||||
run: |
|
||||
git add .
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
git fetch origin
|
||||
echo ${{ github.head_ref }}
|
||||
git checkout ${{ github.head_ref }}
|
||||
# TODO when safari works on ubuntu remove the os part of the commit message
|
||||
git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" || true
|
||||
git push
|
||||
git push origin ${{ github.head_ref }}
|
||||
- name: Run ubuntu/chrome flow
|
||||
run: yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts
|
||||
env:
|
||||
CI: true
|
||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
playwright-macos:
|
||||
timeout-minutes: 60
|
||||
runs-on: macos-latest
|
||||
needs: playwright-ubuntu
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache wasm
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
- name: build wasm
|
||||
run: yarn build:wasm
|
||||
- name: build web
|
||||
run: yarn build:local
|
||||
- name: Run macos/safari flow
|
||||
# safari doesn't work on Ubuntu because of the same reason tauri doesn't (webRTC issues)
|
||||
# TODO remove this and the matrix and run all tests on ubuntu when this is fixed
|
||||
run: yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts
|
||||
env:
|
||||
CI: true
|
||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -33,3 +33,9 @@ src/wasm-lib/bindings
|
||||
src/wasm-lib/kcl/bindings
|
||||
public/wasm_lib_bg.wasm
|
||||
src/wasm-lib/lcov.info
|
||||
|
||||
e2e/playwright/playwright-secrets.env
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
107
README.md
107
README.md
@ -176,3 +176,110 @@ $ cargo +nightly fuzz run parser
|
||||
|
||||
For more information on fuzzing you can check out
|
||||
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
|
||||
|
||||
|
||||
### Playwright
|
||||
|
||||
First time running plawright locally, you'll need to add the secrets file
|
||||
```bash
|
||||
touch ./e2e/playwright/playwright-secrets.env
|
||||
echo 'token="your-token"' > ./e2e/playwright/playwright-secrets.env
|
||||
```
|
||||
then replace "your-token" with a dev token from dev.kittycad.io/account/api-tokens
|
||||
|
||||
then:
|
||||
run playwright
|
||||
```
|
||||
yarn playwright test
|
||||
```
|
||||
|
||||
run a specific test suite
|
||||
```
|
||||
yarn playwright test src/e2e-tests/example.spec.ts
|
||||
```
|
||||
|
||||
run a specific test change the test from `test('...` to `test.only('...`
|
||||
(note if you commit this, the tests will instantly fail without running any of the tests)
|
||||
|
||||
run headed
|
||||
```
|
||||
yarn playwright test --headed
|
||||
```
|
||||
|
||||
run with step through debugger
|
||||
```
|
||||
PWDEBUG=1 yarn playwright test
|
||||
```
|
||||
However, if you want a debugger I recommend using VSCode and the `playwright` extension, as the above command is a cruder debugger that steps into every function call which is annoying.
|
||||
With the extension you can set a breakpoint after `waitForDefaultPlanesVisibilityChange` in order to skip app loading, then the vscode debugger's "step over" is much better for being able to stay at the right level of abstraction as you debug the code.
|
||||
|
||||
If you want to limit to a single browser use `--project="webkit"` or `firefox`, `Google Chrome`
|
||||
Or comment out browsers in `playwright.config.ts`.
|
||||
|
||||
note chromium has encoder compat issues which is why were testing against the branded 'Google Chrome'
|
||||
|
||||
You may consider using the VSCode extension, it's useful for running individual threads, but some some reason the "record a test" is locked to chromium with we can't use. A work around is to us the CI `yarn playwright codegen -b wk --load-storage ./store localhost:3000`
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
|
||||
Where `./store` should look like this
|
||||
|
||||
</summary>
|
||||
|
||||
```JSON
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:3000",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "store",
|
||||
"value": "{\"state\":{\"openPanes\":[\"code\"]},\"version\":0}"
|
||||
},
|
||||
{
|
||||
"name": "persistCode",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"name": "TOKEN_PERSIST_KEY",
|
||||
"value": "your-token"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
However because much of our tests involve clicking in the stream at specific locations, it's code-gen looks `await page.locator('video').click();` when really we need to use a pixel coord, so I think it's of limited use.
|
||||
|
||||
#### Some notes on CI
|
||||
|
||||
The tests are broken into snapshot tests and non-snapshot tests, and they run in that order, they automatically commit new snap shots, so if you see an image commit check it was an intended change. If we have non-determinism in the snapshots such that they are always committing new images, hopefully this annoyance makes us fix them asap, if you notice this happening let Kurt know. But for the odd occasion `git reset --hard HEAD~ && git push -f` is your friend.
|
||||
|
||||
How to interpret failing playwright tests?
|
||||
If your tests fail, click through to the action and see that the tests failed on a line that includes `await page.getByTestId('loading').waitFor({ state: 'detached' })`, this means the test fail because the stream never started. It's you choice if you want to re-run the test, or ignore the failure.
|
||||
|
||||
We run on ubuntu and macos, because safari doesn't work on linux because of the dreaded "no RTCPeerConnection variable" error. But linux runs first and then macos for the same reason that we limit the number of parallel tests to 1 because we limit stream connections per user, so tests would start failing we if let them run together.
|
||||
|
||||
#### Getting started writing a playwright test in our app
|
||||
|
||||
Besides following the instructions above and using the playwright docs, our app is weird because of the whole stream thing, which means our testing is weird. Because we've just figured out this stuff and therefore docs might go stale quick here's a 15min vid/tutorial
|
||||
|
||||
https://github.com/KittyCAD/modeling-app/assets/29681384/6f5e8e85-1003-4fd9-be7f-f36ce833942d
|
||||
|
||||
<details>
|
||||
|
||||
<summary>
|
||||
Ps for the debug panel, the following JSON is useful for snapping the camera
|
||||
</summary>
|
||||
|
||||
```JSON
|
||||
{"type":"modeling_cmd_req","cmd_id":"054e5472-e5e9-4071-92d7-1ce3bac61956","cmd":{"type":"default_camera_look_at","center":{"x":15,"y":0,"z":0},"up":{"x":0,"y":0,"z":1},"vantage":{"x":30,"y":30,"z":30}}}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
394
e2e/playwright/flow-tests.spec.ts
Normal file
394
e2e/playwright/flow-tests.spec.ts
Normal file
@ -0,0 +1,394 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { secrets } from './secrets'
|
||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getUtils } from './test-utils'
|
||||
import waitOn from 'wait-on'
|
||||
|
||||
/*
|
||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||
just from the nature of the stream, running the test with debugger and pasting the below
|
||||
into the console can be useful to get coords
|
||||
|
||||
document.addEventListener('mousemove', (e) =>
|
||||
console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`)
|
||||
)
|
||||
*/
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
// wait for Vite preview server to be up
|
||||
await waitOn({
|
||||
resources: ['tcp:3000'],
|
||||
timeout: 5000,
|
||||
})
|
||||
await context.addInitScript(async (token) => {
|
||||
localStorage.setItem('TOKEN_PERSIST_KEY', token)
|
||||
localStorage.setItem('persistCode', ``)
|
||||
localStorage.setItem(
|
||||
'SETTINGS_PERSIST_KEY',
|
||||
JSON.stringify({
|
||||
baseUnit: 'in',
|
||||
cameraControls: 'KittyCAD',
|
||||
defaultDirectory: '',
|
||||
defaultProjectName: 'project-$nnn',
|
||||
onboardingStatus: 'dismissed',
|
||||
showDebugPanel: true,
|
||||
textWrapping: 'On',
|
||||
theme: 'system',
|
||||
unitSystem: 'imperial',
|
||||
})
|
||||
)
|
||||
}, secrets.token)
|
||||
// kill animations, speeds up tests and reduced flakiness
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||
})
|
||||
|
||||
test.setTimeout(60000)
|
||||
|
||||
test('Basic sketch', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('localhost:3000')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
// select a plane
|
||||
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
|
||||
await u.waitForCmdReceive('set_tool')
|
||||
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Line' }).click(),
|
||||
'set_tool'
|
||||
)
|
||||
|
||||
const startXPx = 600
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
|
||||
'mouse_click',
|
||||
false
|
||||
)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
|
||||
const startAt = '[9.94, -13.41]'
|
||||
const tenish = '10.03'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${tenish}, 0], %)`)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${tenish}, 0], %)
|
||||
|> line([0, ${tenish}], %)`)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${tenish}, 0], %)
|
||||
|> line([0, ${tenish}], %)
|
||||
|> line([-19.97, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Line' }).click(),
|
||||
'set_tool'
|
||||
)
|
||||
|
||||
// click between first two clicks to get center of the line
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10),
|
||||
'select_with_point'
|
||||
)
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// hold down shift
|
||||
await page.keyboard.down('Shift')
|
||||
// click between the latest two clicks to get center of the line
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20)
|
||||
|
||||
// selected two lines therefore there should be two cursors
|
||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||
|
||||
await page.getByRole('button', { name: 'Equal Length' }).click()
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line({ to: [10.03, 0], tag: 'seg01' }, %)
|
||||
|> line([0, ${tenish}], %)
|
||||
|> angledLine([180, segLen('seg01', %)], %)`)
|
||||
})
|
||||
|
||||
test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
await page.goto('localhost:3000')
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// check no error to begin with
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
/* add the following code to the editor (# error is not a valid line)
|
||||
# error
|
||||
const topAng = 30
|
||||
const bottomAng = 25
|
||||
*/
|
||||
await page.click('.cm-content')
|
||||
await page.keyboard.type('# error')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('const topAng = 30')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('const bottomAng = 25')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// error in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(page.getByText("found unknown token '#'")).toBeVisible()
|
||||
|
||||
// select the line that's causing the error and delete it
|
||||
await page.getByText('# error').click()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.down('Shift')
|
||||
await page.keyboard.press('Home')
|
||||
await page.keyboard.up('Shift')
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
// wait for .cm-lint-marker-error not to be visible
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('executes on load', async ({ page, context }) => {
|
||||
const u = getUtils(page)
|
||||
await context.addInitScript(async (token) => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([-6.95, 4.98], %)
|
||||
|> line([25.1, 0.41], %)
|
||||
|> line([0.73, -14.93], %)
|
||||
|> line([-23.44, 0.52], %)`
|
||||
)
|
||||
})
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
await page.goto('localhost:3000')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// expand variables section
|
||||
await page.getByText('Variables').click()
|
||||
|
||||
// can find part001 in the variables summary (pretty-json-container, makes sure we're not looking in the code editor)
|
||||
// part001 only shows up in the variables summary if it's been executed
|
||||
await page.waitForFunction(() => {
|
||||
const variablesElement = document.querySelector(
|
||||
'.pretty-json-container'
|
||||
) as HTMLDivElement
|
||||
return variablesElement.innerHTML.includes('part001')
|
||||
})
|
||||
await expect(
|
||||
page.locator('.pretty-json-container >> text=part001')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('re-executes', async ({ page, context }) => {
|
||||
const u = getUtils(page)
|
||||
await context.addInitScript(async (token) => {
|
||||
localStorage.setItem('persistCode', `const myVar = 5`)
|
||||
})
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
await page.goto('localhost:3000')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await page.getByText('Variables').click()
|
||||
// expect to see "myVar:5"
|
||||
await expect(
|
||||
page.locator('.pretty-json-container >> text=myVar:5')
|
||||
).toBeVisible()
|
||||
|
||||
// change 5 to 67
|
||||
await page.getByText('const myVar').click()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.type('67')
|
||||
|
||||
await expect(
|
||||
page.locator('.pretty-json-container >> text=myVar:67')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can create sketches on all planes and their back sides', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('localhost:3000')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
const camCmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
center: { x: 15, y: 0, z: 0 },
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
vantage: { x: 30, y: 30, z: 30 },
|
||||
},
|
||||
}
|
||||
|
||||
const TestSinglePlane = async ({
|
||||
viewCmd,
|
||||
expectedCode,
|
||||
clickCoords,
|
||||
}: {
|
||||
viewCmd: EngineCommand
|
||||
expectedCode: string
|
||||
clickCoords: { x: number; y: number }
|
||||
}) => {
|
||||
await u.openDebugPanel()
|
||||
await u.sendCustomCmd(viewCmd)
|
||||
await u.clearCommandLogs()
|
||||
// await page.waitForTimeout(200)
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
await u.closeDebugPanel()
|
||||
await page.mouse.click(clickCoords.x, clickCoords.y)
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Line' })).toBeVisible()
|
||||
|
||||
// draw a line
|
||||
const startXPx = 600
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
await u.waitForCmdReceive('set_tool')
|
||||
await u.clearCommandLogs()
|
||||
await u.closeDebugPanel()
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await u.openDebugPanel()
|
||||
await u.waitForCmdReceive('mouse_click')
|
||||
await u.closeDebugPanel()
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(expectedCode)
|
||||
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
|
||||
await u.clearCommandLogs()
|
||||
await u.removeCurrentCode()
|
||||
}
|
||||
|
||||
const codeTemplate = (
|
||||
plane = 'XY',
|
||||
sign = ''
|
||||
) => `const part001 = startSketchOn('${plane}')
|
||||
|> startProfileAt([${sign}3.97, -5.36], %)
|
||||
|> line([${sign}4.01, 0], %)`
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmd,
|
||||
expectedCode: codeTemplate('XY'),
|
||||
clickCoords: { x: 700, y: 350 }, // red plane
|
||||
})
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmd,
|
||||
expectedCode: codeTemplate('YZ'),
|
||||
clickCoords: { x: 1000, y: 200 }, // green plane
|
||||
})
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmd,
|
||||
expectedCode: codeTemplate('XZ', '-'),
|
||||
clickCoords: { x: 630, y: 130 }, // blue plane
|
||||
})
|
||||
|
||||
// new camera angle to click the back side of all three planes
|
||||
const camCmdBackSide: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
center: { x: -15, y: 0, z: 0 },
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
vantage: { x: -30, y: -30, z: -30 },
|
||||
},
|
||||
}
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmdBackSide,
|
||||
expectedCode: codeTemplate('-XY', '-'),
|
||||
clickCoords: { x: 705, y: 136 }, // back of red plane
|
||||
})
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmdBackSide,
|
||||
expectedCode: codeTemplate('-YZ', '-'),
|
||||
clickCoords: { x: 1000, y: 350 }, // back of green plane
|
||||
})
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmdBackSide,
|
||||
expectedCode: codeTemplate('-XZ'),
|
||||
clickCoords: { x: 600, y: 400 }, // back of blue plane
|
||||
})
|
||||
})
|
||||
|
||||
test('Auto complete works', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('localhost:3000')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
// this test might be brittle as we add and remove functions
|
||||
// but should also be easy to update.
|
||||
// tests clicking on an option, selection the first option
|
||||
// and arrowing down to an option
|
||||
|
||||
await page.click('.cm-content')
|
||||
await page.keyboard.type('const part001 = start')
|
||||
|
||||
// expect there to be three auto complete options
|
||||
await expect(page.locator('.cm-completionLabel')).toHaveCount(3)
|
||||
await page.getByText('startSketchOn').click()
|
||||
await page.keyboard.type("('XY')")
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(' |> startProfi')
|
||||
// expect there be a single auto complete option that we can just hit enter on
|
||||
await expect(page.locator('.cm-completionLabel')).toBeVisible()
|
||||
await page.keyboard.press('Enter') // accepting the auto complete, not a new line
|
||||
|
||||
await page.keyboard.type('([0,0], %)')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(' |> lin')
|
||||
|
||||
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible()
|
||||
// press arrow down twice then enter to accept xLine
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('(5, %)')
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([0,0], %)
|
||||
|> xLine(5, %)`)
|
||||
})
|
20
e2e/playwright/secrets.ts
Normal file
20
e2e/playwright/secrets.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
const secrets: Record<string, string> = {}
|
||||
try {
|
||||
const file = readFileSync('./e2e/playwright/playwright-secrets.env', 'utf8')
|
||||
file
|
||||
.split('\n')
|
||||
.filter((line) => line && line.length > 1)
|
||||
.forEach((line) => {
|
||||
const [key, value] = line.split('=')
|
||||
// prefer env vars over secrets file
|
||||
secrets[key] = process.env[key] || (value as any).replaceAll('"', '')
|
||||
})
|
||||
} catch (err) {
|
||||
// probably running in CI
|
||||
secrets.token = process.env.token || ''
|
||||
// add more env vars here to make them available in CI
|
||||
}
|
||||
|
||||
export { secrets }
|
134
e2e/playwright/snapshot-tests.spec.ts
Normal file
134
e2e/playwright/snapshot-tests.spec.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { secrets } from './secrets'
|
||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getUtils } from './test-utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await context.addInitScript(async (token) => {
|
||||
localStorage.setItem('TOKEN_PERSIST_KEY', token)
|
||||
localStorage.setItem('persistCode', ``)
|
||||
localStorage.setItem(
|
||||
'SETTINGS_PERSIST_KEY',
|
||||
JSON.stringify({
|
||||
baseUnit: 'in',
|
||||
cameraControls: 'KittyCAD',
|
||||
defaultDirectory: '',
|
||||
defaultProjectName: 'project-$nnn',
|
||||
onboardingStatus: 'dismissed',
|
||||
showDebugPanel: true,
|
||||
textWrapping: 'On',
|
||||
theme: 'system',
|
||||
unitSystem: 'imperial',
|
||||
})
|
||||
)
|
||||
}, secrets.token)
|
||||
// reducedMotion kills animations, which speeds up tests and reduces flakiness
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||
})
|
||||
|
||||
test.setTimeout(60000)
|
||||
|
||||
test('change camera, show planes', async ({ page, context }) => {
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('localhost:3000')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openAndClearDebugPanel()
|
||||
|
||||
const camCmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
center: { x: 0, y: 0, z: 0 },
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
vantage: { x: 0, y: 50, z: 50 },
|
||||
},
|
||||
}
|
||||
|
||||
await u.sendCustomCmd(camCmd)
|
||||
await u.waitForCmdReceive('default_camera_look_at')
|
||||
|
||||
// rotate
|
||||
await u.closeDebugPanel()
|
||||
await page.mouse.move(700, 200)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.mouse.move(600, 300)
|
||||
await page.mouse.up({ button: 'right' })
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.waitForCmdReceive('camera_drag_end')
|
||||
await page.waitForTimeout(500)
|
||||
await u.clearCommandLogs()
|
||||
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
await u.sendCustomCmd(camCmd)
|
||||
await u.waitForCmdReceive('default_camera_look_at')
|
||||
|
||||
await u.clearCommandLogs()
|
||||
await u.closeDebugPanel()
|
||||
// pan
|
||||
await page.keyboard.down('Shift')
|
||||
await page.mouse.move(600, 200)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.mouse.move(700, 200)
|
||||
await page.mouse.up({ button: 'right' })
|
||||
await page.keyboard.up('Shift')
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.waitForCmdReceive('camera_drag_end')
|
||||
await page.waitForTimeout(300)
|
||||
await u.clearCommandLogs()
|
||||
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
await u.sendCustomCmd(camCmd)
|
||||
await u.waitForCmdReceive('default_camera_look_at')
|
||||
|
||||
await u.clearCommandLogs()
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// zoom
|
||||
await page.keyboard.down('Control')
|
||||
await page.mouse.move(700, 400)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.mouse.move(700, 350)
|
||||
await page.mouse.up({ button: 'right' })
|
||||
await page.keyboard.up('Control')
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.waitForCmdReceive('camera_drag_end')
|
||||
await page.waitForTimeout(300)
|
||||
await u.clearCommandLogs()
|
||||
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
Binary file not shown.
After Width: | Height: | Size: 118 KiB |
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
114
e2e/playwright/test-utils.ts
Normal file
114
e2e/playwright/test-utils.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { expect, Page } from '@playwright/test'
|
||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||
|
||||
async function waitForPageLoad(page: Page) {
|
||||
// wait for 'Loading KittyCAD Modeling App...' spinner
|
||||
await page.getByTestId('initial-load').waitFor()
|
||||
// wait for 'Loading stream...' spinner
|
||||
await page.getByTestId('loading-stream').waitFor()
|
||||
// wait for all spinners to be gone
|
||||
await page.getByTestId('loading').waitFor({ state: 'detached' })
|
||||
|
||||
await page.getByTestId('start-sketch').waitFor()
|
||||
}
|
||||
|
||||
async function removeCurrentCode(page: Page) {
|
||||
const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control'
|
||||
await page.click('.cm-content')
|
||||
await page.keyboard.down(hotkey)
|
||||
await page.keyboard.press('a')
|
||||
await page.keyboard.up(hotkey)
|
||||
await page.keyboard.press('Backspace')
|
||||
await expect(page.locator('.cm-content')).toHaveText('')
|
||||
}
|
||||
|
||||
async function sendCustomCmd(page: Page, cmd: EngineCommand) {
|
||||
await page.fill('[data-testid="custom-cmd-input"]', JSON.stringify(cmd))
|
||||
await page.click('[data-testid="custom-cmd-send-button"]')
|
||||
}
|
||||
|
||||
async function clearCommandLogs(page: Page) {
|
||||
await page.click('[data-testid="clear-commands"]')
|
||||
}
|
||||
|
||||
async function expectCmdLog(page: Page, locatorStr: string) {
|
||||
await expect(page.locator(locatorStr)).toBeVisible()
|
||||
}
|
||||
|
||||
async function waitForDefaultPlanesToBeVisible(page: Page) {
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document.querySelectorAll('[data-receive-command-type="object_visible"]')
|
||||
.length >= 3
|
||||
)
|
||||
}
|
||||
|
||||
async function openDebugPanel(page: Page) {
|
||||
const isOpen =
|
||||
(await page
|
||||
.locator('[data-testid="debug-panel"]')
|
||||
?.getAttribute('open')) === ''
|
||||
|
||||
if (!isOpen) {
|
||||
await page.getByText('Debug').click()
|
||||
await page.getByTestId('debug-panel').and(page.locator('[open]')).waitFor()
|
||||
}
|
||||
}
|
||||
|
||||
async function closeDebugPanel(page: Page) {
|
||||
const isOpen =
|
||||
(await page.getByTestId('debug-panel')?.getAttribute('open')) === ''
|
||||
if (isOpen) {
|
||||
await page.getByText('Debug').click()
|
||||
await page
|
||||
.getByTestId('debug-panel')
|
||||
.and(page.locator(':not([open])'))
|
||||
.waitFor()
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForCmdReceive(page: Page, commandType: string) {
|
||||
return page
|
||||
.locator(`[data-receive-command-type="${commandType}"]`)
|
||||
.first()
|
||||
.waitFor()
|
||||
}
|
||||
|
||||
export function getUtils(page: Page) {
|
||||
return {
|
||||
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
||||
removeCurrentCode: () => removeCurrentCode(page),
|
||||
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
|
||||
clearCommandLogs: () => clearCommandLogs(page),
|
||||
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
|
||||
waitForDefaultPlanesVisibilityChange: () =>
|
||||
waitForDefaultPlanesToBeVisible(page),
|
||||
openDebugPanel: () => openDebugPanel(page),
|
||||
closeDebugPanel: () => closeDebugPanel(page),
|
||||
openAndClearDebugPanel: async () => {
|
||||
await openDebugPanel(page)
|
||||
return clearCommandLogs(page)
|
||||
},
|
||||
clearAndCloseDebugPanel: async () => {
|
||||
await clearCommandLogs(page)
|
||||
return closeDebugPanel(page)
|
||||
},
|
||||
waitForCmdReceive: (commandType: string) =>
|
||||
waitForCmdReceive(page, commandType),
|
||||
doAndWaitForCmd: async (
|
||||
fn: () => Promise<void>,
|
||||
commandType: string,
|
||||
endWithDebugPanelOpen = true
|
||||
) => {
|
||||
await openDebugPanel(page)
|
||||
await clearCommandLogs(page)
|
||||
await closeDebugPanel(page)
|
||||
await fn()
|
||||
await openDebugPanel(page)
|
||||
await waitForCmdReceive(page, commandType)
|
||||
if (!endWithDebugPanelOpen) {
|
||||
await closeDebugPanel(page)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ describe('Modeling App', () => {
|
||||
it('open the sign in page', async () => {
|
||||
const button = await $('#signin')
|
||||
expect(button).toHaveText('Sign in')
|
||||
|
||||
|
||||
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
|
||||
await button.waitForClickable()
|
||||
await browser.execute('arguments[0].click();', button)
|
||||
|
18
package.json
18
package.json
@ -60,6 +60,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"serve": "vite serve --port=3000",
|
||||
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
||||
"build:local": "vite build",
|
||||
"build:both": "vite build",
|
||||
@ -72,8 +73,8 @@
|
||||
"test:e2e": "wdio run wdio.conf.js",
|
||||
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||
"fmt": "prettier --write ./src",
|
||||
"fmt-check": "prettier --check ./src",
|
||||
"fmt": "prettier --write ./src && prettier --write ./e2e",
|
||||
"fmt-check": "prettier --check ./src && prettier --check ./e2e",
|
||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||
"build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
||||
@ -103,16 +104,22 @@
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.23.3",
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@tauri-apps/cli": "^1.5.6",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/debounce-promise": "^3.1.8",
|
||||
"@types/isomorphic-fetch": "^0.0.36",
|
||||
"@types/react-modal": "^3.16.0",
|
||||
"@types/uuid": "^9.0.4",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@types/wicg-file-system-access": "^2020.9.6",
|
||||
"@types/ws": "^8.5.5",
|
||||
"@vitejs/plugin-react": "^4.1.1",
|
||||
"@vitest/coverage-istanbul": "^0.34.6",
|
||||
"@wdio/cli": "^7.7.3",
|
||||
"@wdio/local-runner": "^7.7.3",
|
||||
"@wdio/mocha-framework": "^7.7.3",
|
||||
"@wdio/spec-reporter": "^7.7.3",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
@ -126,10 +133,7 @@
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"yarn": "^1.22.19",
|
||||
"@wdio/cli": "^7.7.3",
|
||||
"@wdio/local-runner": "^7.7.3",
|
||||
"@wdio/mocha-framework": "^7.7.3",
|
||||
"@wdio/spec-reporter": "^7.7.3"
|
||||
"wait-on": "^7.2.0",
|
||||
"yarn": "^1.22.19"
|
||||
}
|
||||
}
|
||||
|
82
playwright.config.ts
Normal file
82
playwright.config.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e/playwright',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // or 'chrome-beta'
|
||||
},
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'chromium', // compat issue with encoding atm, so we're using the branded 'Google Chrome' instead
|
||||
// use: { ...devices['Desktop Chrome'] },
|
||||
// },
|
||||
|
||||
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'yarn serve',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
|
@ -226,7 +226,7 @@ export function App() {
|
||||
<Stream className="absolute inset-0 z-0" />
|
||||
{showDebugPanel && (
|
||||
<DebugPanel
|
||||
title="Debug (AST Explorer)"
|
||||
title="Debug"
|
||||
className={
|
||||
'transition-opacity transition-duration-75 ' +
|
||||
paneOpacity +
|
||||
|
@ -7,7 +7,9 @@ export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||
const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
|
||||
|
||||
return isLoggingIn ? (
|
||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
||||
<Loading>
|
||||
<span data-testid="initial-load">Loading KittyCAD Modeling App...</span>
|
||||
</Loading>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)
|
||||
|
@ -48,7 +48,7 @@ export const Toolbar = () => {
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||
Start Sketch
|
||||
<span data-testid="start-sketch">Start Sketch</span>
|
||||
</button>
|
||||
)}
|
||||
{state.nextEvents.includes('Enter sketch') && pathId && (
|
||||
|
@ -9,6 +9,7 @@ export interface CollapsiblePanelProps
|
||||
icon?: IconDefinition
|
||||
open?: boolean
|
||||
menu?: React.ReactNode
|
||||
detailsTestId?: string
|
||||
iconClassNames?: {
|
||||
bg?: string
|
||||
icon?: string
|
||||
@ -51,11 +52,13 @@ export const CollapsiblePanel = ({
|
||||
className,
|
||||
iconClassNames,
|
||||
menu,
|
||||
detailsTestId,
|
||||
...props
|
||||
}: CollapsiblePanelProps) => {
|
||||
return (
|
||||
<details
|
||||
{...props}
|
||||
data-testid={detailsTestId}
|
||||
className={styles.panel + ' group ' + (className || '')}
|
||||
>
|
||||
<PanelHeader
|
||||
|
@ -1,17 +1,20 @@
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { AstExplorer } from './AstExplorer'
|
||||
import { EngineCommands } from './EngineCommands'
|
||||
|
||||
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||
return (
|
||||
<CollapsiblePanel
|
||||
{...props}
|
||||
className={
|
||||
'!absolute overflow-hidden !h-auto bottom-5 right-5 ' + className
|
||||
'!absolute overflow-auto !h-auto bottom-5 right-5 ' + className
|
||||
}
|
||||
// header height, top-5, and bottom-5
|
||||
style={{ maxHeight: 'calc(100% - 3rem - 1.25rem - 1.25rem)' }}
|
||||
detailsTestId="debug-panel"
|
||||
>
|
||||
<section className="p-4 flex flex-col gap-4">
|
||||
<EngineCommands />
|
||||
<div style={{ height: '400px' }} className="overflow-y-auto">
|
||||
<AstExplorer />
|
||||
</div>
|
||||
|
85
src/components/EngineCommands.tsx
Normal file
85
src/components/EngineCommands.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { CommandLog, engineCommandManager } from 'lang/std/engineConnection'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
function useEngineCommands(): [CommandLog[], () => void] {
|
||||
const [engineCommands, setEngineCommands] = useState<CommandLog[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
engineCommandManager.registerCommandLogCallback((commands) =>
|
||||
setEngineCommands(commands)
|
||||
)
|
||||
}, [])
|
||||
|
||||
return [engineCommands, () => engineCommandManager.clearCommandLogs()]
|
||||
}
|
||||
|
||||
export const EngineCommands = () => {
|
||||
const [engineCommands, clearEngineCommands] = useEngineCommands()
|
||||
const [containsFilter, setContainsFilter] = useState('')
|
||||
const [customCmd, setCustomCmd] = useState('')
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
className="text-gray-800 bg-slate-300 px-2"
|
||||
data-testid="filter-input"
|
||||
type="text"
|
||||
value={containsFilter}
|
||||
onChange={(e) => setContainsFilter(e.target.value)}
|
||||
placeholder="Filter"
|
||||
/>
|
||||
<div className="max-w-xl max-h-36 overflow-auto">
|
||||
{engineCommands.map((command, index) => {
|
||||
const stringer = JSON.stringify(command)
|
||||
if (containsFilter && !stringer.includes(containsFilter)) return null
|
||||
return (
|
||||
<pre className="text-xs">
|
||||
<code
|
||||
key={index}
|
||||
data-message-type={command.type}
|
||||
data-command-type={
|
||||
(command.type === 'send-modeling' ||
|
||||
command.type === 'send-scene') &&
|
||||
command.data.type === 'modeling_cmd_req'
|
||||
? command?.data?.cmd?.type
|
||||
: ''
|
||||
}
|
||||
data-command-id={
|
||||
(command.type === 'send-modeling' ||
|
||||
command.type === 'send-scene') &&
|
||||
command.data.type === 'modeling_cmd_req'
|
||||
? command.data.cmd_id
|
||||
: ''
|
||||
}
|
||||
data-receive-command-type={
|
||||
command.type === 'receive-reliable' ? command.cmd_type : ''
|
||||
}
|
||||
>
|
||||
{JSON.stringify(command, null, 2)}
|
||||
</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<button data-testid="clear-commands" onClick={clearEngineCommands}>
|
||||
Clear
|
||||
</button>
|
||||
<br />
|
||||
<input
|
||||
className="text-gray-800 bg-slate-300 px-2"
|
||||
type="text"
|
||||
value={customCmd}
|
||||
onChange={(e) => setCustomCmd(e.target.value)}
|
||||
placeholder="JSON command"
|
||||
data-testid="custom-cmd-input"
|
||||
/>
|
||||
<button
|
||||
data-testid="custom-cmd-send-button"
|
||||
onClick={() =>
|
||||
engineCommandManager.sendSceneCommand(JSON.parse(customCmd))
|
||||
}
|
||||
>
|
||||
Send custom command
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -10,7 +10,10 @@ const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
return () => clearTimeout(timer)
|
||||
}, [setHasLongLoadTime])
|
||||
return (
|
||||
<div className="body-bg flex flex-col items-center justify-center h-screen">
|
||||
<div
|
||||
className="body-bg flex flex-col items-center justify-center h-screen"
|
||||
data-testid="loading"
|
||||
>
|
||||
<svg viewBox="0 0 10 10" className="w-8 h-8">
|
||||
<circle cx="5" cy="5" r="4" stroke="var(--liquid-20)" fill="none" />
|
||||
<circle
|
||||
|
@ -356,6 +356,8 @@ export const Stream = ({ className = '' }) => {
|
||||
|
||||
kclManager.executeAstMock(modifiedAst, true)
|
||||
})
|
||||
} else {
|
||||
engineCommandManager.sendSceneCommand(command)
|
||||
}
|
||||
|
||||
setDidDragInStream(false)
|
||||
@ -394,7 +396,9 @@ export const Stream = ({ className = '' }) => {
|
||||
/>
|
||||
{isLoading && (
|
||||
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Loading>Loading stream...</Loading>
|
||||
<Loading>
|
||||
<span data-testid="loading-stream">Loading stream...</span>
|
||||
</Loading>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -7,10 +7,11 @@ import { HotkeysProvider } from 'react-hotkeys-hook'
|
||||
import { inspect } from '@xstate/inspect'
|
||||
import { DEV } from 'env'
|
||||
|
||||
if (DEV)
|
||||
inspect({
|
||||
iframe: false,
|
||||
})
|
||||
// uncomment for xstate inspector
|
||||
// if (DEV)
|
||||
// inspect({
|
||||
// iframe: false,
|
||||
// })
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||
|
||||
|
@ -246,6 +246,10 @@ class KclManager {
|
||||
this.code = recast(ast)
|
||||
}
|
||||
this._executeCallback()
|
||||
engineCommandManager.addCommandLog({
|
||||
type: 'execution-done',
|
||||
data: null,
|
||||
})
|
||||
}
|
||||
async executeAstMock(ast: Program = this._ast, updateCode = false) {
|
||||
await this.ensureWasmInit()
|
||||
|
@ -590,12 +590,36 @@ interface Subscription<T extends ModelTypes> {
|
||||
) => void
|
||||
}
|
||||
|
||||
export type CommandLog =
|
||||
| {
|
||||
type: 'send-modeling'
|
||||
data: EngineCommand
|
||||
}
|
||||
| {
|
||||
type: 'send-scene'
|
||||
data: EngineCommand
|
||||
}
|
||||
| {
|
||||
type: 'receive-reliable'
|
||||
data: WebSocketResponse
|
||||
id: string
|
||||
cmd_type?: string
|
||||
}
|
||||
| {
|
||||
type: 'execution-done'
|
||||
data: null
|
||||
}
|
||||
|
||||
type CommandLogTypes = Extract<CommandLog, { type: string }>['type']
|
||||
|
||||
export class EngineCommandManager {
|
||||
artifactMap: ArtifactMap = {}
|
||||
outSequence = 1
|
||||
inSequence = 1
|
||||
engineConnection?: EngineConnection
|
||||
defaultPlanes: DefaultPlanes = { xy: '', yz: '', xz: '' }
|
||||
_commandLogs: CommandLog[] = []
|
||||
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
||||
// Folks should realize that wait for ready does not get called _everytime_
|
||||
// the connection resets and restarts, it only gets called the first time.
|
||||
// Be careful what you put here.
|
||||
@ -786,6 +810,12 @@ export class EngineCommandManager {
|
||||
return
|
||||
}
|
||||
const modelingResponse = message.data.modeling_response
|
||||
this.addCommandLog({
|
||||
type: 'receive-reliable',
|
||||
data: message,
|
||||
id,
|
||||
cmd_type: this.artifactMap[id]?.commandType,
|
||||
})
|
||||
Object.values(this.subscriptions[modelingResponse.type] || {}).forEach(
|
||||
(callback) => callback(modelingResponse)
|
||||
)
|
||||
@ -923,6 +953,21 @@ export class EngineCommandManager {
|
||||
this.engineConnection?.send(deletCmd)
|
||||
})
|
||||
}
|
||||
addCommandLog(message: CommandLog) {
|
||||
if (this._commandLogs.length > 500) {
|
||||
this._commandLogs.shift()
|
||||
}
|
||||
this._commandLogs.push(message)
|
||||
|
||||
this._commandLogCallBack([...this._commandLogs])
|
||||
}
|
||||
clearCommandLogs() {
|
||||
this._commandLogs = []
|
||||
this._commandLogCallBack(this._commandLogs)
|
||||
}
|
||||
registerCommandLogCallback(callback: (command: CommandLog[]) => void) {
|
||||
this._commandLogCallBack = callback
|
||||
}
|
||||
sendSceneCommand(command: EngineCommand): Promise<any> {
|
||||
if (this.engineConnection === undefined) {
|
||||
return Promise.resolve()
|
||||
@ -932,6 +977,20 @@ export class EngineCommandManager {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
command.type === 'modeling_cmd_req' &&
|
||||
(command.cmd.type === 'highlight_set_entity' ||
|
||||
command.cmd.type === 'mouse_move')
|
||||
)
|
||||
) {
|
||||
// highlight_set_entity and mouse_move are sent over the unreliable channel and are too noisy
|
||||
this.addCommandLog({
|
||||
type: 'send-scene',
|
||||
data: command,
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
command.type === 'modeling_cmd_req' &&
|
||||
command.cmd.type !== lastMessage
|
||||
@ -987,6 +1046,17 @@ export class EngineCommandManager {
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (typeof command !== 'string') {
|
||||
this.addCommandLog({
|
||||
type: 'send-modeling',
|
||||
data: command,
|
||||
})
|
||||
} else {
|
||||
this.addCommandLog({
|
||||
type: 'send-modeling',
|
||||
data: JSON.parse(command),
|
||||
})
|
||||
}
|
||||
this.engineConnection?.send(command)
|
||||
if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
|
||||
return this.handlePendingCommand(id, command?.cmd, range)
|
||||
|
@ -26,7 +26,8 @@
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
"src",
|
||||
"e2e"
|
||||
],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import react from '@vitejs/plugin-react'
|
||||
import viteTsconfigPaths from 'vite-tsconfig-paths'
|
||||
import eslint from 'vite-plugin-eslint'
|
||||
import dns from 'dns'
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { defineConfig, configDefaults } from 'vitest/config';
|
||||
|
||||
// Only needed because we run Node < 17
|
||||
// and we want to open `localhost` not `127.0.0.1` on server start
|
||||
@ -21,6 +21,7 @@ const config = defineConfig({
|
||||
coverage: {
|
||||
provider: 'istanbul' // or 'v8'
|
||||
},
|
||||
exclude: [...configDefaults.exclude, '**/e2e/playwright/**/*'],
|
||||
},
|
||||
build: {
|
||||
outDir: 'build',
|
||||
|
139
yarn.lock
139
yarn.lock
@ -1518,6 +1518,18 @@
|
||||
dependencies:
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@hapi/hoek@^9.0.0":
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
|
||||
integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==
|
||||
|
||||
"@hapi/topo@^5.0.0":
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012"
|
||||
integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@headlessui/react@^1.7.17":
|
||||
version "1.7.17"
|
||||
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.17.tgz#a0ec23af21b527c030967245fd99776aa7352bc6"
|
||||
@ -1736,6 +1748,13 @@
|
||||
strict-event-emitter-types "^2.0.0"
|
||||
ws "^7.0.0"
|
||||
|
||||
"@playwright/test@^1.39.0":
|
||||
version "1.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.39.0.tgz#d10ba8e38e44104499e25001945f07faa9fa91cd"
|
||||
integrity sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==
|
||||
dependencies:
|
||||
playwright "1.39.0"
|
||||
|
||||
"@react-hook/latest@^1.0.2":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-hook/latest/-/latest-1.0.3.tgz#c2d1d0b0af8b69ec6e2b3a2412ba0768ac82db80"
|
||||
@ -1840,6 +1859,23 @@
|
||||
dependencies:
|
||||
"@sentry/types" "7.77.0"
|
||||
|
||||
"@sideway/address@^4.1.3":
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0"
|
||||
integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@sideway/formula@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
|
||||
integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
|
||||
|
||||
"@sideway/pinpoint@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
|
||||
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
|
||||
|
||||
"@sinclair/typebox@^0.24.1":
|
||||
version "0.24.51"
|
||||
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f"
|
||||
@ -2357,6 +2393,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.4.tgz#e884a59338da907bda8d2ed03e01c5c49d036f1c"
|
||||
integrity sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==
|
||||
|
||||
"@types/wait-on@^5.3.4":
|
||||
version "5.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/wait-on/-/wait-on-5.3.4.tgz#5ee270b3e073fb01073f9f044922c6893de8c4d2"
|
||||
integrity sha512-EBsPjFMrFlMbbUFf9D1Fp+PAB2TwmUn7a3YtHyD9RLuTIk1jDd8SxXVAoez2Ciy+8Jsceo2MYEYZzJ/DvorOKw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/which@^1.3.2":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf"
|
||||
@ -3024,6 +3067,11 @@ async@^3.2.3, async@^3.2.4:
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
|
||||
integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
autoprefixer@^10.4.13:
|
||||
version "10.4.14"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d"
|
||||
@ -3053,6 +3101,15 @@ axios@^0.26.1:
|
||||
dependencies:
|
||||
follow-redirects "^1.14.8"
|
||||
|
||||
axios@^1.6.1:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2"
|
||||
integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.0"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
axobject-query@^3.1.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a"
|
||||
@ -3497,6 +3554,13 @@ color-name@~1.1.4:
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
||||
@ -3760,6 +3824,11 @@ define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0:
|
||||
has-property-descriptors "^1.0.0"
|
||||
object-keys "^1.1.1"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
dequal@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||
@ -4534,6 +4603,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.8:
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
||||
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
|
||||
|
||||
follow-redirects@^1.15.0:
|
||||
version "1.15.3"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
|
||||
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
|
||||
|
||||
for-each@^0.3.3:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
|
||||
@ -4541,6 +4615,15 @@ for-each@^0.3.3:
|
||||
dependencies:
|
||||
is-callable "^1.1.3"
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
||||
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formdata-polyfill@^4.0.10:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
|
||||
@ -4592,7 +4675,7 @@ fs.realpath@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
fsevents@~2.3.2:
|
||||
fsevents@2.3.2, fsevents@~2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
@ -5492,6 +5575,17 @@ jiti@^1.19.1:
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
|
||||
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
|
||||
|
||||
joi@^17.11.0:
|
||||
version "17.11.0"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-17.11.0.tgz#aa9da753578ec7720e6f0ca2c7046996ed04fc1a"
|
||||
integrity sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
"@hapi/topo" "^5.0.0"
|
||||
"@sideway/address" "^4.1.3"
|
||||
"@sideway/formula" "^3.0.1"
|
||||
"@sideway/pinpoint" "^2.0.0"
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@ -5848,6 +5942,18 @@ micromatch@^4.0.4, micromatch@^4.0.5:
|
||||
braces "^3.0.2"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
mime@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
@ -5908,7 +6014,7 @@ minimatch@~3.0.2:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
|
||||
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
@ -6433,6 +6539,20 @@ pkg-types@^1.0.3:
|
||||
mlly "^1.2.0"
|
||||
pathe "^1.1.0"
|
||||
|
||||
playwright-core@1.39.0:
|
||||
version "1.39.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.39.0.tgz#efeaea754af4fb170d11845b8da30b2323287c63"
|
||||
integrity sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==
|
||||
|
||||
playwright@1.39.0:
|
||||
version "1.39.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.39.0.tgz#184c81cd6478f8da28bcd9e60e94fcebf566e077"
|
||||
integrity sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==
|
||||
dependencies:
|
||||
playwright-core "1.39.0"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
portfinder@^1.0.28:
|
||||
version "1.0.32"
|
||||
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81"
|
||||
@ -6566,7 +6686,7 @@ prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
proxy-from-env@1.1.0, proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
@ -7010,7 +7130,7 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
rxjs@^7.2.0, rxjs@^7.5.5:
|
||||
rxjs@^7.2.0, rxjs@^7.5.5, rxjs@^7.8.1:
|
||||
version "7.8.1"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
|
||||
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
|
||||
@ -7912,6 +8032,17 @@ w3c-keyname@^2.2.4:
|
||||
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
|
||||
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
|
||||
|
||||
wait-on@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-7.2.0.tgz#d76b20ed3fc1e2bebc051fae5c1ff93be7892928"
|
||||
integrity sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==
|
||||
dependencies:
|
||||
axios "^1.6.1"
|
||||
joi "^17.11.0"
|
||||
lodash "^4.17.21"
|
||||
minimist "^1.2.8"
|
||||
rxjs "^7.8.1"
|
||||
|
||||
warning@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
|
Reference in New Issue
Block a user