Show when user can't connect because of a bad token (#2105)

* Reapply "Add ping pong health, remove a timeout interval, fix up netwo… (#1771)

This reverts commit 1913519f68.

* Fix build errors

* Add new error states to network status notification

* Remove unused variable

* Refactor to use Context API for network status

* Don't do any stream events if network is not ok

* Catch LSP errors on bad auth

* Show when authentication is bad (cookie header only)

* Fix formatting

* Fix up types

* Revert awaiting on lsp failure

* Fix tsc

* wip

* wip

* fmt

* Fix typing

* Incorporate ping health; yarn make:dev; faster video stream loss notice

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

* run ci pls

* run ci pls

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

* run ci pls again

* Remove unused variables

* Add new instructions on running Playwright anywhere

* Please the Playwright. Praise the Playwright.

* Correct a vitest

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

* ci again

* Fix tests unrelated to this PR

* Fix flakiness in for segments tests

* Bump to 2 workers

* fmt

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

* fmt

* fmt

* Fixups

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

* ci

* Set workers to 1

* Wait for network status listeners before connecting

* Fix initial connection requirements and trying 2 workers again

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
49fl
2024-06-04 08:32:24 -04:00
committed by GitHub
parent c551d88db4
commit f8a1f40f20
33 changed files with 1247 additions and 804 deletions

14
Makefile Normal file
View File

@ -0,0 +1,14 @@
.PHONY: dev
WASM_LIB_FILES := $(wildcard src/wasm-lib/**/*.rs)
dev: node_modules public/wasm_lib_bg.wasm
yarn start
public/wasm_lib_bg.wasm: $(WASM_LIB_FILES)
yarn build:wasm-dev
node_modules: package.json
package.json:
yarn install

View File

@ -197,28 +197,32 @@ For more information on fuzzing you can check out
### Playwright ### Playwright
First time running plawright locally, you'll need to add the secrets file For a portable way to run Playwright you'll need Docker.
After that, open a terminal and run:
```bash ```bash
touch ./e2e/playwright/playwright-secrets.env docker run --network host --rm --init -it playwright/chrome:playwright-1.43.1
printf 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets.env
``` ```
and in another terminal, run:
```bash
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" <test suite>
```
An example of a `<test suite>` is: `e2e/playwright/flow-tests.spec.ts`
YOU WILL NEED A PLAYWRIGHT-SECRETS.ENV FILE:
```bash
# ./e2e/playwright/playwright-secrets.env
token=<your-token>
snapshottoken=<your-snapshot-token>
```
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens then replace "your-token" with a dev token from dev.zoo.dev/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('...` 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) (note if you commit this, the tests will instantly fail without running any of the tests)

View File

@ -1,5 +1,11 @@
import { test, expect, Page } from '@playwright/test' import { test, expect, Page } from '@playwright/test'
import { makeTemplate, getUtils, doExport } from './test-utils' import {
makeTemplate,
getUtils,
getMovementUtils,
wiggleMove,
doExport,
} from './test-utils'
import waitOn from 'wait-on' import waitOn from 'wait-on'
import { XOR, roundOff, uuidv4 } from 'lib/utils' import { XOR, roundOff, uuidv4 } from 'lib/utils'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes' import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
@ -27,6 +33,8 @@ document.addEventListener('mousemove', (e) =>
) )
*/ */
const deg = (Math.PI * 2) / 360
const commonPoints = { const commonPoints = {
startAt: '[9.06, -12.22]', startAt: '[9.06, -12.22]',
num1: 9.14, num1: 9.14,
@ -35,6 +43,8 @@ const commonPoints = {
// num2: 19.19, // num2: 19.19,
} }
// Utilities for writing tests that depend on test values
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
// wait for Vite preview server to be up // wait for Vite preview server to be up
await waitOn({ await waitOn({
@ -1484,12 +1494,15 @@ const part001 = startSketchOn('XZ')
test('Can add multiple sketches', async ({ page }) => { test('Can add multiple sketches', async ({ page }) => {
test.skip(process.platform === 'darwin', 'Can add multiple sketches') test.skip(process.platform === 'darwin', 'Can add multiple sketches')
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) const viewportSize = { width: 1200, height: 500 }
const PUR = 400 / 37.5 //pixeltoUnitRatio await page.setViewportSize(viewportSize)
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
const { toSU, click00r, expectCodeToBe } = getMovementUtils({ center, page })
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -1502,127 +1515,71 @@ test('Can add multiple sketches', async ({ page }) => {
200 200
) )
// select a plane let codeStr = "const part001 = startSketchOn('XY')"
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('XZ')`
)
await page.mouse.click(center.x, viewportSize.height * 0.55)
await expectCodeToBe(codeStr)
await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
const startXPx = 600 await click00r(0, 0)
await u.closeDebugPanel() codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await expectCodeToBe(codeStr)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await click00r(50, 0)
await page.waitForTimeout(100) codeStr += ` |> line(${toSU([50, 0])}, %)`
await expectCodeToBe(codeStr)
await expect(page.locator('.cm-content')) await click00r(0, 50)
.toHaveText(`const part001 = startSketchOn('XZ') codeStr += ` |> line(${toSU([0, 50])}, %)`
|> startProfileAt(${commonPoints.startAt}, %) await expectCodeToBe(codeStr)
|> line([${commonPoints.num1}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await click00r(-50, 0)
await expect(page.locator('.cm-content')) codeStr += ` |> line(${toSU([-50, 0])}, %)`
.toHaveText(`const part001 = startSketchOn('XZ') await expectCodeToBe(codeStr)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
const finalCodeFirstSketch = `const part001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)
|> line([-${commonPoints.num2}, 0], %)`
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
// exit the sketch
// exit the sketch, reset relative clicker
click00r(undefined, undefined)
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.updateCamPosition([100, 100, 100])
await page.waitForTimeout(250) await page.waitForTimeout(250)
await u.clearCommandLogs()
// start a new sketch // start a new sketch
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(400)
await page.mouse.click(650, 450)
// when exiting the sketch above the camera is still looking down at XY,
// so selecting the plane again is a bit easier.
await page.mouse.click(center.x + 30, center.y)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await u.clearAndCloseDebugPanel() codeStr += "const part002 = startSketchOn('XY')"
await expectCodeToBe(codeStr)
// on mock os there are issues with getting the camera to update
// it should not be selecting the 'XZ' plane here if the camera updated
// properly, but if we just role with it we can still verify everything
// in the rest of the test
const plane = process.platform === 'darwin' ? 'XZ' : 'XY'
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt2 =
process.platform === 'darwin' ? '[9.75, -13.16]' : '[0.93, -1.25]'
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
)
await page.waitForTimeout(100)
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num2 = process.platform === 'darwin' ? 9.84 : 0.94 await click00r(30, 0)
await expect( codeStr += ` |> startProfileAt(${toSU([30, 0])}, %)`
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') await expectCodeToBe(codeStr)
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)`.replace(/\s/g, '')
)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await click00r(30, 0)
await expect( codeStr += ` |> line(${toSU([30 - 0.1 /* imprecision */, 0])}, %)`
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') await expectCodeToBe(codeStr)
).toBe(
`${finalCodeFirstSketch} await click00r(0, 30)
const part002 = startSketchOn('${plane}') codeStr += ` |> line(${toSU([0, 30])}, %)`
|> startProfileAt(${startAt2}, %) await expectCodeToBe(codeStr)
|> line([${num2}, 0], %)
|> line([0, ${roundOff( await click00r(-30, 0)
num2 + (process.platform === 'darwin' ? 0.01 : -0.01) codeStr += ` |> line(${toSU([-30 + 0.1, 0])}, %)`
)}], %)`.replace(/\s/g, '') await expectCodeToBe(codeStr)
)
await page.waitForTimeout(100) click00r(undefined, undefined)
await page.mouse.click(startXPx, 500 - PUR * 20) await u.openAndClearDebugPanel()
await expect( await page.getByRole('button', { name: 'Exit Sketch' }).click()
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') await u.expectCmdLog('[data-message-type="execution-done"]')
).toBe( await u.updateCamPosition([100, 100, 100])
`${finalCodeFirstSketch} await page.waitForTimeout(250)
const part002 = startSketchOn('${plane}') await u.clearCommandLogs()
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)
|> line([0, ${roundOff(
num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
)}], %)
|> line([-${process.platform === 'darwin' ? 19.59 : 1.87}, 0], %)`.replace(
/\s/g,
''
)
)
}) })
test('ProgramMemory can be serialised', async ({ page }) => { test('ProgramMemory can be serialised', async ({ page }) => {
@ -3246,7 +3203,7 @@ test.describe('Testing segment overlays', () => {
expectAfterUnconstrained, expectAfterUnconstrained,
expectFinal, expectFinal,
ang = 45, ang = 45,
steps = 6, steps = 10,
}: { }: {
hoverPos: { x: number; y: number } hoverPos: { x: number; y: number }
constraintType: constraintType:
@ -3261,13 +3218,16 @@ test.describe('Testing segment overlays', () => {
steps?: number steps?: number
}) => { }) => {
await expect(page.getByText('Added variable')).not.toBeVisible() await expect(page.getByText('Added variable')).not.toBeVisible()
const [x, y] = [
Math.cos((ang * Math.PI) / 180) * 45,
Math.sin((ang * Math.PI) / 180) * 45,
]
await page.mouse.move(hoverPos.x + x, hoverPos.y + y) await page.mouse.move(0, 0)
await page.mouse.move(hoverPos.x, hoverPos.y, { steps }) await page.waitForTimeout(1000)
let x = 0,
y = 0
x = hoverPos.x + Math.cos(ang * deg) * 32
y = hoverPos.y - Math.sin(ang * deg) * 32
await page.mouse.move(x, y)
await wiggleMove(page, x, y, 20, 30, ang, 10, 5)
await expect(page.locator('.cm-content')).toContainText( await expect(page.locator('.cm-content')).toContainText(
expectBeforeUnconstrained expectBeforeUnconstrained
) )
@ -3283,6 +3243,14 @@ test.describe('Testing segment overlays', () => {
await expect(page.locator('.cm-content')).toContainText( await expect(page.locator('.cm-content')).toContainText(
expectAfterUnconstrained expectAfterUnconstrained
) )
await page.mouse.move(0, 0)
await page.waitForTimeout(1000)
x = hoverPos.x + Math.cos(ang * deg) * 32
y = hoverPos.y - Math.sin(ang * deg) * 32
await page.mouse.move(x, y)
await wiggleMove(page, x, y, 20, 30, ang, 10, 5)
const unconstrainedLocator = page.locator( const unconstrainedLocator = page.locator(
`[data-constraint-type="${constraintType}"][data-is-constrained="false"]` `[data-constraint-type="${constraintType}"][data-is-constrained="false"]`
) )
@ -3328,14 +3296,16 @@ test.describe('Testing segment overlays', () => {
ang?: number ang?: number
steps?: number steps?: number
}) => { }) => {
const [x, y] = [ await page.mouse.move(0, 0)
Math.cos((ang * Math.PI) / 180) * 45, await page.waitForTimeout(1000)
Math.sin((ang * Math.PI) / 180) * 45, let x = 0,
] y = 0
await page.mouse.move(hoverPos.x + x, hoverPos.y + y) x = hoverPos.x + Math.cos(ang * deg) * 32
y = hoverPos.y - Math.sin(ang * deg) * 32
await page.mouse.move(x, y)
await wiggleMove(page, x, y, 20, 30, ang, 10, 5)
await expect(page.getByText('Added variable')).not.toBeVisible() await expect(page.getByText('Added variable')).not.toBeVisible()
await page.mouse.move(hoverPos.x, hoverPos.y, { steps })
await expect(page.locator('.cm-content')).toContainText( await expect(page.locator('.cm-content')).toContainText(
expectBeforeUnconstrained expectBeforeUnconstrained
) )
@ -3353,7 +3323,14 @@ test.describe('Testing segment overlays', () => {
expectAfterUnconstrained expectAfterUnconstrained
) )
await expect(page.getByText('Added variable')).not.toBeVisible() await expect(page.getByText('Added variable')).not.toBeVisible()
await page.mouse.move(hoverPos.x, hoverPos.y, { steps })
await page.mouse.move(0, 0)
await page.waitForTimeout(1000)
x = hoverPos.x + Math.cos(ang * deg) * 32
y = hoverPos.y - Math.sin(ang * deg) * 32
await page.mouse.move(x, y)
await wiggleMove(page, x, y, 20, 30, ang, 10, 5)
const constrainedLocator = page.locator( const constrainedLocator = page.locator(
`[data-constraint-type="${constraintType}"][data-is-constrained="true"]` `[data-constraint-type="${constraintType}"][data-is-constrained="true"]`
) )
@ -3365,6 +3342,7 @@ test.describe('Testing segment overlays', () => {
await constrainedLocator.click() await constrainedLocator.click()
await expect(page.locator('.cm-content')).toContainText(expectFinal) await expect(page.locator('.cm-content')).toContainText(expectFinal)
} }
test.setTimeout(120000)
test('for segments [line, angledLine, lineTo, xLineTo]', async ({ test('for segments [line, angledLine, lineTo, xLineTo]', async ({
page, page,
}) => { }) => {
@ -3372,24 +3350,24 @@ test.describe('Testing segment overlays', () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`const part001 = startSketchOn('XZ') `const part001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %) |> startProfileAt([5 + 0, 20 + 0], %)
|> line([0.5, -14 + 0], %) |> line([0.5, -14 + 0], %)
|> angledLine({ angle: 3 + 0, length: 32 + 0 }, %) |> angledLine({ angle: 3 + 0, length: 32 + 0 }, %)
|> lineTo([33, 11.5 + 0], %) |> lineTo([5 + 33, 20 + 11.5 + 0], %)
|> xLineTo(9 - 5, %) |> xLineTo(5 + 9 - 5, %)
|> yLineTo(-10.77, %, 'a') |> yLineTo(20 + -10.77, %, 'a')
|> xLine(26.04, %) |> xLine(26.04, %)
|> yLine(21.14 + 0, %) |> yLine(21.14 + 0, %)
|> angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %) |> angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %)
|> angledLineOfYLength({ angle: -91, length: 19 + 0 }, %) |> angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)
|> angledLineToX({ angle: 3 + 0, to: 26 }, %) |> angledLineToX({ angle: 3 + 0, to: 5 + 26 }, %)
|> angledLineToY({ angle: 89, to: 9.14 + 0 }, %) |> angledLineToY({ angle: 89, to: 20 + 9.14 + 0 }, %)
|> angledLineThatIntersects({ |> angledLineThatIntersects({
angle: 4.14, angle: 4.14,
intersectTag: 'a', intersectTag: 'a',
offset: 9 offset: 9
}, %) }, %)
|> tangentialArcTo([3.14 + 13, 3.14], %) |> tangentialArcTo([5 + 3.14 + 13, 20 + 3.14], %)
` `
) )
}) })
@ -3423,8 +3401,9 @@ test.describe('Testing segment overlays', () => {
}, },
}) })
await page.waitForTimeout(100) await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.getByText('xLineTo(9 - 5, %)').click() await page.getByText('xLineTo(5 + 9 - 5, %)').click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500) await page.waitForTimeout(500)
@ -3434,45 +3413,62 @@ test.describe('Testing segment overlays', () => {
const clickUnconstrained = _clickUnconstrained(page) const clickUnconstrained = _clickUnconstrained(page)
const clickConstrained = _clickConstrained(page) const clickConstrained = _clickConstrained(page)
// Drag the sketch into view
await page.mouse.move(600, 64)
await page.mouse.down({ button: 'middle' })
await page.mouse.move(600, 450, { steps: 10 })
await page.mouse.up({ button: 'middle' })
await page.mouse.move(600, 64)
await page.mouse.down({ button: 'middle' })
await page.mouse.move(600, 120, { steps: 10 })
await page.mouse.up({ button: 'middle' })
let ang = 0
const line = await u.getBoundingBox(`[data-overlay-index="${0}"]`) const line = await u.getBoundingBox(`[data-overlay-index="${0}"]`)
console.log('line1') ang = await u.getAngle(`[data-overlay-index="${0}"]`)
console.log('line1', line, ang)
await clickConstrained({ await clickConstrained({
hoverPos: { x: line.x, y: line.y - 10 }, hoverPos: { x: line.x, y: line.y },
constraintType: 'yRelative', constraintType: 'yRelative',
expectBeforeUnconstrained: '|> line([0.5, -14 + 0], %)', expectBeforeUnconstrained: '|> line([0.5, -14 + 0], %)',
expectAfterUnconstrained: '|> line([0.5, -14], %)', expectAfterUnconstrained: '|> line([0.5, -14], %)',
expectFinal: '|> line([0.5, yRel001], %)', expectFinal: '|> line([0.5, yRel001], %)',
ang: 135, ang: ang + 180,
}) })
console.log('line2') console.log('line2')
await clickUnconstrained({ await clickUnconstrained({
hoverPos: { x: line.x, y: line.y - 10 }, hoverPos: { x: line.x, y: line.y },
constraintType: 'xRelative', constraintType: 'xRelative',
expectBeforeUnconstrained: '|> line([0.5, yRel001], %)', expectBeforeUnconstrained: '|> line([0.5, yRel001], %)',
expectAfterUnconstrained: 'line([xRel001, yRel001], %)', expectAfterUnconstrained: 'line([xRel001, yRel001], %)',
expectFinal: '|> line([0.5, yRel001], %)', expectFinal: '|> line([0.5, yRel001], %)',
ang: -45, ang: ang + 180,
}) })
const angledLine = await u.getBoundingBox(`[data-overlay-index="1"]`) const angledLine = await u.getBoundingBox(`[data-overlay-index="1"]`)
ang = await u.getAngle(`[data-overlay-index="1"]`)
console.log('angledLine1') console.log('angledLine1')
await clickConstrained({ await clickConstrained({
hoverPos: { x: angledLine.x - 10, y: angledLine.y }, hoverPos: { x: angledLine.x, y: angledLine.y },
constraintType: 'angle', constraintType: 'angle',
expectBeforeUnconstrained: expectBeforeUnconstrained:
'angledLine({ angle: 3 + 0, length: 32 + 0 }, %)', 'angledLine({ angle: 3 + 0, length: 32 + 0 }, %)',
expectAfterUnconstrained: 'angledLine({ angle: 3, length: 32 + 0 }, %)', expectAfterUnconstrained: 'angledLine({ angle: 3, length: 32 + 0 }, %)',
expectFinal: 'angledLine({ angle: angle001, length: 32 + 0 }, %)', expectFinal: 'angledLine({ angle: angle001, length: 32 + 0 }, %)',
ang: ang + 180,
}) })
console.log('angledLine2') console.log('angledLine2')
await clickConstrained({ await clickConstrained({
hoverPos: { x: angledLine.x - 10, y: angledLine.y }, hoverPos: { x: angledLine.x, y: angledLine.y },
constraintType: 'length', constraintType: 'length',
expectBeforeUnconstrained: expectBeforeUnconstrained:
'angledLine({ angle: angle001, length: 32 + 0 }, %)', 'angledLine({ angle: angle001, length: 32 + 0 }, %)',
expectAfterUnconstrained: expectAfterUnconstrained:
'angledLine({ angle: angle001, length: 32 }, %)', 'angledLine({ angle: angle001, length: 32 }, %)',
expectFinal: 'angledLine({ angle: angle001, length: len001 }, %)', expectFinal: 'angledLine({ angle: angle001, length: len001 }, %)',
ang: ang + 180,
}) })
await page.mouse.move(700, 250) await page.mouse.move(700, 250)
@ -3482,36 +3478,39 @@ test.describe('Testing segment overlays', () => {
} }
await page.waitForTimeout(200) await page.waitForTimeout(200)
const lineTo = await u.getBoundingBox(`[data-overlay-index="2"]`) let lineTo = await u.getBoundingBox(`[data-overlay-index="2"]`)
ang = await u.getAngle(`[data-overlay-index="2"]`)
console.log('lineTo1') console.log('lineTo1')
await clickConstrained({ await clickConstrained({
hoverPos: { x: lineTo.x, y: lineTo.y + 21 }, hoverPos: { x: lineTo.x, y: lineTo.y },
constraintType: 'yAbsolute', constraintType: 'yAbsolute',
expectBeforeUnconstrained: 'lineTo([33, 11.5 + 0], %)', expectBeforeUnconstrained: 'lineTo([5 + 33, 20 + 11.5 + 0], %)',
expectAfterUnconstrained: 'lineTo([33, 11.5], %)', expectAfterUnconstrained: 'lineTo([5 + 33, 31.5], %)',
expectFinal: 'lineTo([33, yAbs001], %)', expectFinal: 'lineTo([5 + 33, yAbs001], %)',
steps: 8, steps: 8,
ang: 55, ang: ang + 180,
}) })
console.log('lineTo2') console.log('lineTo2')
await clickUnconstrained({ await clickConstrained({
hoverPos: { x: lineTo.x, y: lineTo.y + 25 }, hoverPos: { x: lineTo.x, y: lineTo.y },
constraintType: 'xAbsolute', constraintType: 'xAbsolute',
expectBeforeUnconstrained: 'lineTo([33, yAbs001], %)', expectBeforeUnconstrained: 'lineTo([5 + 33, yAbs001], %)',
expectAfterUnconstrained: 'lineTo([xAbs001, yAbs001], %)', expectAfterUnconstrained: 'lineTo([38, yAbs001], %)',
expectFinal: 'lineTo([33, yAbs001], %)', expectFinal: 'lineTo([xAbs001, yAbs001], %)',
steps: 8, steps: 8,
ang: ang + 180,
}) })
const xLineTo = await u.getBoundingBox(`[data-overlay-index="3"]`) const xLineTo = await u.getBoundingBox(`[data-overlay-index="3"]`)
ang = await u.getAngle(`[data-overlay-index="3"]`)
console.log('xlineTo1') console.log('xlineTo1')
await clickConstrained({ await clickConstrained({
hoverPos: { x: xLineTo.x + 15, y: xLineTo.y }, hoverPos: { x: xLineTo.x, y: xLineTo.y },
constraintType: 'xAbsolute', constraintType: 'xAbsolute',
expectBeforeUnconstrained: 'xLineTo(9 - 5, %)', expectBeforeUnconstrained: 'xLineTo(5 + 9 - 5, %)',
expectAfterUnconstrained: 'xLineTo(4, %)', expectAfterUnconstrained: 'xLineTo(9, %)',
expectFinal: 'xLineTo(xAbs002, %)', expectFinal: 'xLineTo(xAbs002, %)',
ang: -45, ang: ang + 180,
steps: 8, steps: 8,
}) })
}) })
@ -3566,26 +3565,31 @@ const part001 = startSketchOn('XZ')
await page.waitForTimeout(300) await page.waitForTimeout(300)
let ang = 0
const yLineTo = await u.getBoundingBox(`[data-overlay-index="4"]`) const yLineTo = await u.getBoundingBox(`[data-overlay-index="4"]`)
ang = await u.getAngle(`[data-overlay-index="4"]`)
console.log('ylineTo1') console.log('ylineTo1')
await clickUnconstrained({ await clickUnconstrained({
hoverPos: { x: yLineTo.x, y: yLineTo.y - 30 }, hoverPos: { x: yLineTo.x, y: yLineTo.y },
constraintType: 'yAbsolute', constraintType: 'yAbsolute',
expectBeforeUnconstrained: "yLineTo(-10.77, %, 'a')", expectBeforeUnconstrained: "yLineTo(-10.77, %, 'a')",
expectAfterUnconstrained: "yLineTo(yAbs002, %, 'a')", expectAfterUnconstrained: "yLineTo(yAbs002, %, 'a')",
expectFinal: "yLineTo(-10.77, %, 'a')", expectFinal: "yLineTo(-10.77, %, 'a')",
ang: ang + 180,
}) })
const xLine = await u.getBoundingBox(`[data-overlay-index="5"]`) const xLine = await u.getBoundingBox(`[data-overlay-index="5"]`)
ang = await u.getAngle(`[data-overlay-index="5"]`)
console.log('xline') console.log('xline')
await clickUnconstrained({ await clickUnconstrained({
hoverPos: { x: xLine.x - 25, y: xLine.y }, hoverPos: { x: xLine.x, y: xLine.y },
constraintType: 'xRelative', constraintType: 'xRelative',
expectBeforeUnconstrained: 'xLine(26.04, %)', expectBeforeUnconstrained: 'xLine(26.04, %)',
expectAfterUnconstrained: 'xLine(xRel002, %)', expectAfterUnconstrained: 'xLine(xRel002, %)',
expectFinal: 'xLine(26.04, %)', expectFinal: 'xLine(26.04, %)',
steps: 10, steps: 10,
ang: 50, ang: ang + 180,
}) })
}) })
test('for segments [yLine, angledLineOfXLength, angledLineOfYLength]', async ({ test('for segments [yLine, angledLineOfXLength, angledLineOfYLength]', async ({
@ -3625,6 +3629,7 @@ const part001 = startSketchOn('XZ')
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()
await page.waitForTimeout(500)
await page.getByText('xLineTo(9 - 5, %)').click() await page.getByText('xLineTo(9 - 5, %)').click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -3636,7 +3641,10 @@ const part001 = startSketchOn('XZ')
const clickUnconstrained = _clickUnconstrained(page) const clickUnconstrained = _clickUnconstrained(page)
const clickConstrained = _clickConstrained(page) const clickConstrained = _clickConstrained(page)
let ang = 0
const yLine = await u.getBoundingBox(`[data-overlay-index="6"]`) const yLine = await u.getBoundingBox(`[data-overlay-index="6"]`)
ang = await u.getAngle(`[data-overlay-index="6"]`)
console.log('yline1') console.log('yline1')
await clickConstrained({ await clickConstrained({
hoverPos: { x: yLine.x, y: yLine.y + 20 }, hoverPos: { x: yLine.x, y: yLine.y + 20 },
@ -3644,11 +3652,13 @@ const part001 = startSketchOn('XZ')
expectBeforeUnconstrained: 'yLine(21.14 + 0, %)', expectBeforeUnconstrained: 'yLine(21.14 + 0, %)',
expectAfterUnconstrained: 'yLine(21.14, %)', expectAfterUnconstrained: 'yLine(21.14, %)',
expectFinal: 'yLine(yRel001, %)', expectFinal: 'yLine(yRel001, %)',
ang: ang + 180,
}) })
const angledLineOfXLength = await u.getBoundingBox( const angledLineOfXLength = await u.getBoundingBox(
`[data-overlay-index="7"]` `[data-overlay-index="7"]`
) )
ang = await u.getAngle(`[data-overlay-index="7"]`)
console.log('angledLineOfXLength1') console.log('angledLineOfXLength1')
await clickConstrained({ await clickConstrained({
hoverPos: { x: angledLineOfXLength.x + 20, y: angledLineOfXLength.y }, hoverPos: { x: angledLineOfXLength.x + 20, y: angledLineOfXLength.y },
@ -3659,6 +3669,7 @@ const part001 = startSketchOn('XZ')
'angledLineOfXLength({ angle: -179, length: 23.14 }, %)', 'angledLineOfXLength({ angle: -179, length: 23.14 }, %)',
expectFinal: expectFinal:
'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)', 'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)',
ang: ang + 180,
}) })
console.log('angledLineOfXLength2') console.log('angledLineOfXLength2')
await clickUnconstrained({ await clickUnconstrained({
@ -3671,11 +3682,13 @@ const part001 = startSketchOn('XZ')
expectFinal: expectFinal:
'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)', 'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)',
steps: 7, steps: 7,
ang: ang + 180,
}) })
const angledLineOfYLength = await u.getBoundingBox( const angledLineOfYLength = await u.getBoundingBox(
`[data-overlay-index="8"]` `[data-overlay-index="8"]`
) )
ang = await u.getAngle(`[data-overlay-index="8"]`)
console.log('angledLineOfYLength1') console.log('angledLineOfYLength1')
await clickUnconstrained({ await clickUnconstrained({
hoverPos: { x: angledLineOfYLength.x, y: angledLineOfYLength.y - 20 }, hoverPos: { x: angledLineOfYLength.x, y: angledLineOfYLength.y - 20 },
@ -3685,7 +3698,7 @@ const part001 = startSketchOn('XZ')
expectAfterUnconstrained: expectAfterUnconstrained:
'angledLineOfYLength({ angle: angle002, length: 19 + 0 }, %)', 'angledLineOfYLength({ angle: angle002, length: 19 + 0 }, %)',
expectFinal: 'angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)', expectFinal: 'angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)',
ang: 135, ang: ang + 180,
steps: 6, steps: 6,
}) })
console.log('angledLineOfYLength2') console.log('angledLineOfYLength2')
@ -3697,14 +3710,13 @@ const part001 = startSketchOn('XZ')
expectAfterUnconstrained: expectAfterUnconstrained:
'angledLineOfYLength({ angle: -91, length: 19 }, %)', 'angledLineOfYLength({ angle: -91, length: 19 }, %)',
expectFinal: 'angledLineOfYLength({ angle: -91, length: yRel002 }, %)', expectFinal: 'angledLineOfYLength({ angle: -91, length: yRel002 }, %)',
ang: -45, ang: ang + 180,
steps: 7, steps: 7,
}) })
}) })
test('for segments [angledLineToX, angledLineToY, angledLineThatIntersects]', async ({ test('for segments [angledLineToX, angledLineToY, angledLineThatIntersects]', async ({
page, page,
}) => { }) => {
test.skip(process.platform !== 'darwin', 'too flakey on ubuntu')
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
@ -3750,14 +3762,18 @@ const part001 = startSketchOn('XZ')
const clickUnconstrained = _clickUnconstrained(page) const clickUnconstrained = _clickUnconstrained(page)
const clickConstrained = _clickConstrained(page) const clickConstrained = _clickConstrained(page)
let ang = 0
const angledLineToX = await u.getBoundingBox(`[data-overlay-index="9"]`) const angledLineToX = await u.getBoundingBox(`[data-overlay-index="9"]`)
ang = await u.getAngle(`[data-overlay-index="9"]`)
console.log('angledLineToX') console.log('angledLineToX')
await clickConstrained({ await clickConstrained({
hoverPos: { x: angledLineToX.x - 20, y: angledLineToX.y }, hoverPos: { x: angledLineToX.x, y: angledLineToX.y },
constraintType: 'angle', constraintType: 'angle',
expectBeforeUnconstrained: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)', expectBeforeUnconstrained: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)',
expectAfterUnconstrained: 'angledLineToX({ angle: 3, to: 26 }, %)', expectAfterUnconstrained: 'angledLineToX({ angle: 3, to: 26 }, %)',
expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)', expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)',
ang: ang + 180,
}) })
console.log('angledLineToX2') console.log('angledLineToX2')
await clickUnconstrained({ await clickUnconstrained({
@ -3768,12 +3784,14 @@ const part001 = startSketchOn('XZ')
expectAfterUnconstrained: expectAfterUnconstrained:
'angledLineToX({ angle: angle001, to: xAbs001 }, %)', 'angledLineToX({ angle: angle001, to: xAbs001 }, %)',
expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)', expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)',
ang: ang + 180,
}) })
const angledLineToY = await u.getBoundingBox(`[data-overlay-index="10"]`) const angledLineToY = await u.getBoundingBox(`[data-overlay-index="10"]`)
ang = await u.getAngle(`[data-overlay-index="10"]`)
console.log('angledLineToY') console.log('angledLineToY')
await clickUnconstrained({ await clickUnconstrained({
hoverPos: { x: angledLineToY.x, y: angledLineToY.y + 20 }, hoverPos: { x: angledLineToY.x, y: angledLineToY.y },
constraintType: 'angle', constraintType: 'angle',
expectBeforeUnconstrained: expectBeforeUnconstrained:
'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)', 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
@ -3781,7 +3799,7 @@ const part001 = startSketchOn('XZ')
'angledLineToY({ angle: angle002, to: 9.14 + 0 }, %)', 'angledLineToY({ angle: angle002, to: 9.14 + 0 }, %)',
expectFinal: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)', expectFinal: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
steps: process.platform === 'darwin' ? 8 : 9, steps: process.platform === 'darwin' ? 8 : 9,
ang: 135, ang: ang + 180,
}) })
console.log('angledLineToY2') console.log('angledLineToY2')
await clickConstrained({ await clickConstrained({
@ -3791,12 +3809,13 @@ const part001 = startSketchOn('XZ')
'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)', 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
expectAfterUnconstrained: 'angledLineToY({ angle: 89, to: 9.14 }, %)', expectAfterUnconstrained: 'angledLineToY({ angle: 89, to: 9.14 }, %)',
expectFinal: 'angledLineToY({ angle: 89, to: yAbs001 }, %)', expectFinal: 'angledLineToY({ angle: 89, to: yAbs001 }, %)',
ang: 135, ang: ang + 180,
}) })
const angledLineThatIntersects = await u.getBoundingBox( const angledLineThatIntersects = await u.getBoundingBox(
`[data-overlay-index="11"]` `[data-overlay-index="11"]`
) )
ang = await u.getAngle(`[data-overlay-index="11"]`)
console.log('angledLineThatIntersects') console.log('angledLineThatIntersects')
await clickUnconstrained({ await clickUnconstrained({
hoverPos: { hoverPos: {
@ -3819,7 +3838,7 @@ const part001 = startSketchOn('XZ')
offset: 9, offset: 9,
intersectTag: 'a' intersectTag: 'a'
}, %)`, }, %)`,
ang: -45, ang: ang + 180,
}) })
console.log('angledLineThatIntersects2') console.log('angledLineThatIntersects2')
await clickUnconstrained({ await clickUnconstrained({
@ -3843,7 +3862,7 @@ const part001 = startSketchOn('XZ')
offset: 9, offset: 9,
intersectTag: 'a' intersectTag: 'a'
}, %)`, }, %)`,
ang: -25, ang: ang + 180,
}) })
}) })
test('for segment [tangentialArcTo]', async ({ page }) => { test('for segment [tangentialArcTo]', async ({ page }) => {
@ -3895,24 +3914,25 @@ const part001 = startSketchOn('XZ')
const tangentialArcTo = await u.getBoundingBox( const tangentialArcTo = await u.getBoundingBox(
`[data-overlay-index="12"]` `[data-overlay-index="12"]`
) )
let ang = await u.getAngle(`[data-overlay-index="12"]`)
console.log('tangentialArcTo') console.log('tangentialArcTo')
await clickConstrained({ await clickConstrained({
hoverPos: { x: tangentialArcTo.x - 10, y: tangentialArcTo.y + 20 }, hoverPos: { x: tangentialArcTo.x, y: tangentialArcTo.y },
constraintType: 'xAbsolute', constraintType: 'xAbsolute',
expectBeforeUnconstrained: 'tangentialArcTo([3.14 + 13, -3.14], %)', expectBeforeUnconstrained: 'tangentialArcTo([3.14 + 13, -3.14], %)',
expectAfterUnconstrained: 'tangentialArcTo([16.14, -3.14], %)', expectAfterUnconstrained: 'tangentialArcTo([16.14, -3.14], %)',
expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)', expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)',
ang: -45, ang: ang + 180,
steps: 6, steps: 6,
}) })
console.log('tangentialArcTo2') console.log('tangentialArcTo2')
await clickUnconstrained({ await clickUnconstrained({
hoverPos: { x: tangentialArcTo.x - 10, y: tangentialArcTo.y + 20 }, hoverPos: { x: tangentialArcTo.x, y: tangentialArcTo.y },
constraintType: 'yAbsolute', constraintType: 'yAbsolute',
expectBeforeUnconstrained: 'tangentialArcTo([xAbs001, -3.14], %)', expectBeforeUnconstrained: 'tangentialArcTo([xAbs001, -3.14], %)',
expectAfterUnconstrained: 'tangentialArcTo([xAbs001, yAbs001], %)', expectAfterUnconstrained: 'tangentialArcTo([xAbs001, yAbs001], %)',
expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)', expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)',
ang: -135, ang: ang + 180,
steps: 10, steps: 10,
}) })
}) })
@ -4007,25 +4027,7 @@ const part001 = startSketchOn('XZ')
steps: 6, steps: 6,
}) })
segmentToDelete = await getOverlayByIndex(0) segmentToDelete = await getOverlayByIndex(11)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 20 },
codeToBeDeleted: 'line([0.5, -14 + 0], %)',
stdLibFnName: 'line',
ang: -45,
})
segmentToDelete = await getOverlayByIndex(0)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x - 20, y: segmentToDelete.y },
codeToBeDeleted: 'angledLine({ angle: 3 + 0, length: 32 + 0 }, %)',
stdLibFnName: 'angledLine',
ang: 135,
})
await page.waitForTimeout(200)
segmentToDelete = await getOverlayByIndex(9)
await deleteSegmentSequence({ await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y }, hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
codeToBeDeleted: `angledLineThatIntersects({ codeToBeDeleted: `angledLineThatIntersects({
@ -4038,21 +4040,21 @@ const part001 = startSketchOn('XZ')
steps: 7, steps: 7,
}) })
segmentToDelete = await getOverlayByIndex(8) segmentToDelete = await getOverlayByIndex(10)
await deleteSegmentSequence({ await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y }, hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
codeToBeDeleted: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)', codeToBeDeleted: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
stdLibFnName: 'angledLineToY', stdLibFnName: 'angledLineToY',
}) })
segmentToDelete = await getOverlayByIndex(7) segmentToDelete = await getOverlayByIndex(9)
await deleteSegmentSequence({ await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y }, hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y },
codeToBeDeleted: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)', codeToBeDeleted: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)',
stdLibFnName: 'angledLineToX', stdLibFnName: 'angledLineToX',
}) })
segmentToDelete = await getOverlayByIndex(6) segmentToDelete = await getOverlayByIndex(8)
await deleteSegmentSequence({ await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 }, hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 },
codeToBeDeleted: codeToBeDeleted:
@ -4060,7 +4062,7 @@ const part001 = startSketchOn('XZ')
stdLibFnName: 'angledLineOfYLength', stdLibFnName: 'angledLineOfYLength',
}) })
segmentToDelete = await getOverlayByIndex(5) segmentToDelete = await getOverlayByIndex(7)
await deleteSegmentSequence({ await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y }, hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
codeToBeDeleted: codeToBeDeleted:
@ -4068,42 +4070,36 @@ const part001 = startSketchOn('XZ')
stdLibFnName: 'angledLineOfXLength', stdLibFnName: 'angledLineOfXLength',
}) })
segmentToDelete = await getOverlayByIndex(4) segmentToDelete = await getOverlayByIndex(6)
await deleteSegmentSequence({ await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y + 10 }, hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y + 10 },
codeToBeDeleted: 'yLine(21.14 + 0, %)', codeToBeDeleted: 'yLine(21.14 + 0, %)',
stdLibFnName: 'yLine', stdLibFnName: 'yLine',
}) })
segmentToDelete = await getOverlayByIndex(3) segmentToDelete = await getOverlayByIndex(5)
await deleteSegmentSequence({ await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y }, hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y },
codeToBeDeleted: 'xLine(26.04, %)', codeToBeDeleted: 'xLine(26.04, %)',
stdLibFnName: 'xLine', stdLibFnName: 'xLine',
}) })
segmentToDelete = await getOverlayByIndex(2) segmentToDelete = await getOverlayByIndex(4)
await deleteSegmentSequence({ await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 }, hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 },
codeToBeDeleted: "yLineTo(-10.77, %, 'a')", codeToBeDeleted: "yLineTo(-10.77, %, 'a')",
stdLibFnName: 'yLineTo', stdLibFnName: 'yLineTo',
}) })
segmentToDelete = await getOverlayByIndex(1) segmentToDelete = await getOverlayByIndex(3)
await deleteSegmentSequence({ await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y }, hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
codeToBeDeleted: 'xLineTo(9 - 5, %)', codeToBeDeleted: 'xLineTo(9 - 5, %)',
stdLibFnName: 'xLineTo', stdLibFnName: 'xLineTo',
}) })
for (let i = 0; i < 15; i++) { // Not sure why this is diff. from the others - Kurt, ideas?
await page.mouse.wheel(0, 100) segmentToDelete = await getOverlayByIndex(2)
await page.waitForTimeout(25)
}
await page.waitForTimeout(200)
segmentToDelete = await getOverlayByIndex(0)
const hoverPos = { x: segmentToDelete.x - 10, y: segmentToDelete.y + 10 } const hoverPos = { x: segmentToDelete.x - 10, y: segmentToDelete.y + 10 }
await expect(page.getByText('Added variable')).not.toBeVisible() await expect(page.getByText('Added variable')).not.toBeVisible()
const [x, y] = [ const [x, y] = [
@ -4122,6 +4118,24 @@ const part001 = startSketchOn('XZ')
await expect(page.locator('.cm-content')).not.toContainText( await expect(page.locator('.cm-content')).not.toContainText(
codeToBeDeleted codeToBeDeleted
) )
segmentToDelete = await getOverlayByIndex(1)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x - 20, y: segmentToDelete.y },
codeToBeDeleted: 'angledLine({ angle: 3 + 0, length: 32 + 0 }, %)',
stdLibFnName: 'angledLine',
ang: 135,
})
segmentToDelete = await getOverlayByIndex(0)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 20 },
codeToBeDeleted: 'line([0.5, -14 + 0], %)',
stdLibFnName: 'line',
ang: -45,
})
await page.waitForTimeout(200)
}) })
}) })
test.describe('Testing delete with dependent segments', () => { test.describe('Testing delete with dependent segments', () => {
@ -4541,6 +4555,11 @@ test('simulate network down and network little widget', async ({ page }) => {
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// This is how we wait until the stream is online
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
const networkWidget = page.locator('[data-testid="network-toggle"]') const networkWidget = page.locator('[data-testid="network-toggle"]')
await expect(networkWidget).toBeVisible() await expect(networkWidget).toBeVisible()
await networkWidget.hover() await networkWidget.hover()
@ -4548,7 +4567,7 @@ test('simulate network down and network little widget', async ({ page }) => {
const networkPopover = page.locator('[data-testid="network-popover"]') const networkPopover = page.locator('[data-testid="network-popover"]')
await expect(networkPopover).not.toBeVisible() await expect(networkPopover).not.toBeVisible()
// Expect the network to be up // (First check) Expect the network to be up
await expect(page.getByText('Network Health (Connected)')).toBeVisible() await expect(page.getByText('Network Health (Connected)')).toBeVisible()
// Click the network widget // Click the network widget
@ -4580,7 +4599,7 @@ test('simulate network down and network little widget', async ({ page }) => {
await expect(networkPopover).toBeVisible() await expect(networkPopover).toBeVisible()
// Click off the modal. // Click off the modal.
await page.mouse.click(100, 100) await page.mouse.click(0, 0)
await expect(networkPopover).not.toBeVisible() await expect(networkPopover).not.toBeVisible()
// Turn back on the network // Turn back on the network
@ -4592,7 +4611,11 @@ test('simulate network down and network little widget', async ({ page }) => {
uploadThroughput: -1, uploadThroughput: -1,
}) })
// Expect the network to be up await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// (Second check) expect the network to be up
await expect(page.getByText('Network Health (Connected)')).toBeVisible() await expect(page.getByText('Network Health (Connected)')).toBeVisible()
}) })
@ -4606,8 +4629,7 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled({ timeout: 15000 })
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button // click on "Start Sketch" button
await u.clearCommandLogs() await u.clearCommandLogs()
@ -4670,6 +4692,10 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
}) })
// Wait for the app to be ready for use // Wait for the app to be ready for use
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// Expect the network to be up // Expect the network to be up
await expect(page.getByText('Network Health (Connected)')).toBeVisible() await expect(page.getByText('Network Health (Connected)')).toBeVisible()
@ -4697,15 +4723,15 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('XZ') .toHaveText(`const part001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([-11.59, 11.1], %)`) |> line([-11.64, 11.11], %)`)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('XZ') .toHaveText(`const part001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([-11.59, 11.1], %) |> line([-11.64, 11.11], %)
|> line([-6.61, 0], %)`) |> line([-6.56, 0], %)`)
// Unequip line tool // Unequip line tool
await page.keyboard.press('Escape') await page.keyboard.press('Escape')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -96,6 +96,79 @@ async function waitForCmdReceive(page: Page, commandType: string) {
.waitFor() .waitFor()
} }
export const wiggleMove = async (
page: any,
x: number,
y: number,
steps: number,
dist: number,
ang: number,
amplitude: number,
freq: number
) => {
const tau = Math.PI * 2
const deg = tau / 360
const step = dist / steps
for (let i = 0, j = 0; i < dist; i += step, j += 1) {
const [x1, y1] = [0, Math.sin((tau / steps) * j * freq) * amplitude]
const [x2, y2] = [
Math.cos(-ang * deg) * i - Math.sin(-ang * deg) * y1,
Math.sin(-ang * deg) * i + Math.cos(-ang * deg) * y1,
]
const [xr, yr] = [x2, y2]
await page.mouse.move(x + xr, y + yr, { steps: 2 })
}
}
export const getMovementUtils = (opts: any) => {
// The way we truncate is kinda odd apparently, so we need this function
// "[k]itty[c]ad round"
const kcRound = (n: number) => Math.trunc(n * 100) / 100
// To translate between screen and engine ("[U]nit") coordinates
// NOTE: these pretty much can't be perfect because of screen scaling.
// Handle on a case-by-case.
const toU = (x: number, y: number) => [
kcRound(x * 0.0854),
kcRound(-y * 0.0854), // Y is inverted in our coordinate system
]
// Turn the array into a string with specific formatting
const fromUToString = (xy: number[]) => `[${xy[0]}, ${xy[1]}]`
// Combine because used often
const toSU = (xy: number[]) => fromUToString(toU(xy[0], xy[1]))
// Make it easier to click around from center ("click [from] zero zero")
const click00 = (x: number, y: number) =>
opts.page.mouse.click(opts.center.x + x, opts.center.y + y)
// Relative clicker, must keep state
let last = { x: 0, y: 0 }
const click00r = (x?: number, y?: number) => {
// reset relative coordinates when anything is undefined
if (x === undefined || y === undefined) {
last.x = 0
last.y = 0
return
}
const ret = click00(last.x + x, last.y + y)
last.x += x
last.y += y
// Returns the new absolute coordinate if you need it.
return ret.then(() => [last.x, last.y])
}
const expectCodeToBe = async (str: string) => {
await expect(opts.page.locator('.cm-content')).toHaveText(str)
await opts.page.waitForTimeout(100)
}
return { toSU, click00r, expectCodeToBe }
}
export async function getUtils(page: Page) { export async function getUtils(page: Page) {
// Chrome devtools protocol session only works in Chromium // Chrome devtools protocol session only works in Chromium
const browserType = page.context().browser()?.browserType().name() const browserType = page.context().browser()?.browserType().name()
@ -145,11 +218,15 @@ export async function getUtils(page: Page) {
y: bbox.y - angleYOffset, y: bbox.y - angleYOffset,
} }
}, },
getAngle: async (locator: string) => {
const overlay = page.locator(locator)
return Number(await overlay.getAttribute('data-overlay-angle'))
},
getBoundingBox: async (locator: string) => getBoundingBox: async (locator: string) =>
page page
.locator(locator) .locator(locator)
.boundingBox() .boundingBox()
.then((box) => ({ x: box?.x || 0, y: box?.y || 0 })), .then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
doAndWaitForCmd: async ( doAndWaitForCmd: async (
fn: () => Promise<void>, fn: () => Promise<void>,
commandType: string, commandType: string,

View File

@ -10,7 +10,7 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.63", "@kittycad/lib": "^0.0.64",
"@lezer/javascript": "^1.4.9", "@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1", "@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^2.0.1", "@react-hook/resize-observer": "^2.0.1",
@ -95,7 +95,8 @@
"lint": "eslint --fix src", "lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json", "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
"postinstall": "yarn xstate:typegen", "postinstall": "yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"" "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
"make:dev": "make dev"
}, },
"prettier": { "prettier": {
"trailingComma": "es5", "trailingComma": "es5",

View File

@ -12,12 +12,12 @@ import { defineConfig, devices } from '@playwright/test'
export default defineConfig({ export default defineConfig({
testDir: './e2e/playwright', testDir: './e2e/playwright',
/* Run tests in files in parallel */ /* Run tests in files in parallel */
fullyParallel: true, fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */ /* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
/* Retry on CI only */ /* Retry on CI only */
retries: process.env.CI ? 3 : 0, retries: process.env.CI ? 3 : 0,
/* Opt out of parallel tests on CI. */ /* Different amount of parallelism on CI and local. */
workers: process.env.CI ? 1 : 1, workers: process.env.CI ? 1 : 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html', reporter: 'html',
@ -72,7 +72,7 @@ export default defineConfig({
/* Run your local dev server before starting the tests */ /* Run your local dev server before starting the tests */
webServer: { webServer: {
command: 'yarn serve', command: 'yarn start',
// url: 'http://127.0.0.1:3000', // url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },

View File

@ -12,6 +12,8 @@ import SignIn from './routes/SignIn'
import { Auth } from './Auth' import { Auth } from './Auth'
import { isTauri } from './lib/isTauri' import { isTauri } from './lib/isTauri'
import Home from './routes/Home' import Home from './routes/Home'
import { NetworkContext } from './hooks/useNetworkContext'
import { useNetworkStatus } from './hooks/useNetworkStatus'
import makeUrlPathRelative from './lib/makeUrlPathRelative' import makeUrlPathRelative from './lib/makeUrlPathRelative'
import DownloadAppBanner from 'components/DownloadAppBanner' import DownloadAppBanner from 'components/DownloadAppBanner'
import { WasmErrBanner } from 'components/WasmErrBanner' import { WasmErrBanner } from 'components/WasmErrBanner'
@ -155,5 +157,11 @@ const router = createBrowserRouter([
* @returns RouterProvider * @returns RouterProvider
*/ */
export const Router = () => { export const Router = () => {
return <RouterProvider router={router} /> const networkStatus = useNetworkStatus()
return (
<NetworkContext.Provider value={networkStatus}>
<RouterProvider router={router} />
</NetworkContext.Provider>
)
} }

View File

@ -3,13 +3,11 @@ import { isCursorInSketchCommandRange } from 'lang/util'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { isSingleCursorInPipe } from 'lang/queryAst' import { isSingleCursorInPipe } from 'lang/queryAst'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import {
NetworkHealthState,
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { ActionButtonDropdown } from 'components/ActionButtonDropdown' import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
@ -38,14 +36,16 @@ export function Toolbar({
}, [engineCommandManager.artifactMap, context.selectionRanges]) }, [engineCommandManager.artifactMap, context.selectionRanges])
const toolbarButtonsRef = useRef<HTMLUListElement>(null) const toolbarButtonsRef = useRef<HTMLUListElement>(null)
const { overallState } = useNetworkContext()
const { overallState } = useNetworkStatus()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
const { isStreamReady } = useStore((s) => ({ const { isStreamReady } = useStore((s) => ({
isStreamReady: s.isStreamReady, isStreamReady: s.isStreamReady,
})) }))
const disableAllButtons = const disableAllButtons =
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady (overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) ||
isExecuting ||
!isStreamReady
useHotkeys( useHotkeys(
'l', 'l',

View File

@ -1,14 +1,65 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import {
EngineConnectionStateType,
DisconnectingType,
EngineCommandManagerEvents,
EngineConnectionEvents,
ConnectionError,
CONNECTION_ERROR_TEXT,
} from '../lang/std/engineConnection'
import { engineCommandManager } from '../lib/singletons'
const Loading = ({ children }: React.PropsWithChildren) => { const Loading = ({ children }: React.PropsWithChildren) => {
const [hasLongLoadTime, setHasLongLoadTime] = useState(false) const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
useEffect(() => { useEffect(() => {
const onConnectionStateChange = ({ detail: state }: CustomEvent) => {
if (
(state.type !== EngineConnectionStateType.Disconnected ||
state.type !== EngineConnectionStateType.Disconnecting) &&
state.value?.type !== DisconnectingType.Error
)
return
setError(state.value.value.error)
}
const onEngineAvailable = ({ detail: engineConnection }: CustomEvent) => {
engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStateChanged,
onConnectionStateChange as EventListener
)
}
engineCommandManager.addEventListener(
EngineCommandManagerEvents.EngineAvailable,
onEngineAvailable as EventListener
)
return () => {
engineCommandManager.removeEventListener(
EngineCommandManagerEvents.EngineAvailable,
onEngineAvailable as EventListener
)
engineCommandManager.engineConnection?.removeEventListener(
EngineConnectionEvents.ConnectionStateChanged,
onConnectionStateChange as EventListener
)
}
}, [])
useEffect(() => {
// Don't set long loading time if there's a more severe error
if (error > ConnectionError.LongLoadingTime) return
const timer = setTimeout(() => { const timer = setTimeout(() => {
setHasLongLoadTime(true) setError(ConnectionError.LongLoadingTime)
}, 4000) }, 4000)
return () => clearTimeout(timer) return () => clearTimeout(timer)
}, [setHasLongLoadTime]) }, [error, setError])
return ( return (
<div <div
className="body-bg flex flex-col items-center justify-center h-screen" className="body-bg flex flex-col items-center justify-center h-screen"
@ -29,10 +80,10 @@ const Loading = ({ children }: React.PropsWithChildren) => {
<p <p
className={ className={
'text-sm mt-4 text-primary/60 transition-opacity duration-500' + 'text-sm mt-4 text-primary/60 transition-opacity duration-500' +
(hasLongLoadTime ? ' opacity-100' : ' opacity-0') (error !== ConnectionError.Unset ? ' opacity-100' : ' opacity-0')
} }
> >
Loading is taking longer than expected. {CONNECTION_ERROR_TEXT[error]}
</p> </p>
</div> </div>
) )

View File

@ -13,7 +13,6 @@ import { LanguageSupport } from '@codemirror/language'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { FileEntry } from 'lib/types' import { FileEntry } from 'lib/types'
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
import Worker from 'editor/plugins/lsp/worker.ts?worker' import Worker from 'editor/plugins/lsp/worker.ts?worker'
import { import {
LspWorkerEventType, LspWorkerEventType,
@ -23,6 +22,8 @@ import {
} from 'editor/plugins/lsp/types' } from 'editor/plugins/lsp/types'
import { wasmUrl } from 'lang/wasm' import { wasmUrl } from 'lang/wasm'
import { PROJECT_ENTRYPOINT } from 'lib/constants' import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] { function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return [] return []
@ -86,7 +87,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
} = useSettingsAuthContext() } = useSettingsAuthContext()
const token = auth?.context.token const token = auth?.context.token
const navigate = useNavigate() const navigate = useNavigate()
const { overallState } = useNetworkStatus() const { overallState } = useNetworkContext()
const isNetworkOkay = overallState === NetworkHealthState.Ok const isNetworkOkay = overallState === NetworkHealthState.Ok
// So this is a bit weird, we need to initialize the lsp server and client. // So this is a bit weird, we need to initialize the lsp server and client.

View File

@ -5,8 +5,8 @@ import { CommandBarProvider } from './CommandBar/CommandBarProvider'
import { import {
NETWORK_HEALTH_TEXT, NETWORK_HEALTH_TEXT,
NetworkHealthIndicator, NetworkHealthIndicator,
NetworkHealthState,
} from './NetworkHealthIndicator' } from './NetworkHealthIndicator'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
function TestWrap({ children }: { children: React.ReactNode }) { function TestWrap({ children }: { children: React.ReactNode }) {
// wrap in router and xState context // wrap in router and xState context
@ -19,6 +19,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
) )
} }
// Our Playwright tests for this are much more comprehensive.
describe('NetworkHealthIndicator tests', () => { describe('NetworkHealthIndicator tests', () => {
test('Renders the network indicator', () => { test('Renders the network indicator', () => {
render( render(
@ -29,21 +30,7 @@ describe('NetworkHealthIndicator tests', () => {
fireEvent.click(screen.getByTestId('network-toggle')) fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network')).toHaveTextContent( // Starts as disconnected
NETWORK_HEALTH_TEXT[NetworkHealthState.Ok]
)
})
test('Responds to network changes', () => {
render(
<TestWrap>
<NetworkHealthIndicator />
</TestWrap>
)
fireEvent.offline(window)
fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network')).toHaveTextContent( expect(screen.getByTestId('network')).toHaveTextContent(
NETWORK_HEALTH_TEXT[NetworkHealthState.Disconnected] NETWORK_HEALTH_TEXT[NetworkHealthState.Disconnected]
) )

View File

@ -1,26 +1,13 @@
import { Popover } from '@headlessui/react' import { Popover } from '@headlessui/react'
import { useEffect, useState } from 'react'
import { ActionIcon, ActionIconProps } from './ActionIcon' import { ActionIcon, ActionIconProps } from './ActionIcon'
import {
ConnectingType,
ConnectingTypeGroup,
DisconnectingType,
EngineConnectionState,
EngineConnectionStateType,
ErrorType,
initialConnectingTypeGroupState,
} from '../lang/std/engineConnection'
import { engineCommandManager } from '../lib/singletons'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
export enum NetworkHealthState { import { useNetworkContext } from '../hooks/useNetworkContext'
Ok, import { NetworkHealthState } from '../hooks/useNetworkStatus'
Issue,
Disconnected,
}
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = { export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
[NetworkHealthState.Ok]: 'Connected', [NetworkHealthState.Ok]: 'Connected',
[NetworkHealthState.Weak]: 'Weak',
[NetworkHealthState.Issue]: 'Problem', [NetworkHealthState.Issue]: 'Problem',
[NetworkHealthState.Disconnected]: 'Offline', [NetworkHealthState.Disconnected]: 'Offline',
} }
@ -61,6 +48,10 @@ const overallConnectionStateColor: Record<NetworkHealthState, IconColorConfig> =
icon: 'text-succeed-80 dark:text-succeed-10', icon: 'text-succeed-80 dark:text-succeed-10',
bg: 'bg-succeed-10/30 dark:bg-succeed-80/50', bg: 'bg-succeed-10/30 dark:bg-succeed-80/50',
}, },
[NetworkHealthState.Weak]: {
icon: 'text-warn-80 dark:text-warn-10',
bg: 'bg-warn-10 dark:bg-warn-80/80',
},
[NetworkHealthState.Issue]: { [NetworkHealthState.Issue]: {
icon: 'text-destroy-80 dark:text-destroy-10', icon: 'text-destroy-80 dark:text-destroy-10',
bg: 'bg-destroy-10 dark:bg-destroy-80/80', bg: 'bg-destroy-10 dark:bg-destroy-80/80',
@ -76,125 +67,11 @@ const overallConnectionStateIcon: Record<
ActionIconProps['icon'] ActionIconProps['icon']
> = { > = {
[NetworkHealthState.Ok]: 'network', [NetworkHealthState.Ok]: 'network',
[NetworkHealthState.Weak]: 'network',
[NetworkHealthState.Issue]: 'networkCrossedOut', [NetworkHealthState.Issue]: 'networkCrossedOut',
[NetworkHealthState.Disconnected]: 'networkCrossedOut', [NetworkHealthState.Disconnected]: 'networkCrossedOut',
} }
export function useNetworkStatus() {
const [steps, setSteps] = useState(initialConnectingTypeGroupState)
const [internetConnected, setInternetConnected] = useState<boolean>(true)
const [overallState, setOverallState] = useState<NetworkHealthState>(
NetworkHealthState.Ok
)
const [hasCopied, setHasCopied] = useState<boolean>(false)
const [error, setError] = useState<ErrorType | undefined>(undefined)
const issues: Record<ConnectingTypeGroup, boolean> = {
[ConnectingTypeGroup.WebSocket]: steps[ConnectingTypeGroup.WebSocket].some(
(a: [ConnectingType, boolean | undefined]) => a[1] === false
),
[ConnectingTypeGroup.ICE]: steps[ConnectingTypeGroup.ICE].some(
(a: [ConnectingType, boolean | undefined]) => a[1] === false
),
[ConnectingTypeGroup.WebRTC]: steps[ConnectingTypeGroup.WebRTC].some(
(a: [ConnectingType, boolean | undefined]) => a[1] === false
),
}
const hasIssues: boolean =
issues[ConnectingTypeGroup.WebSocket] ||
issues[ConnectingTypeGroup.ICE] ||
issues[ConnectingTypeGroup.WebRTC]
useEffect(() => {
setOverallState(
!internetConnected
? NetworkHealthState.Disconnected
: hasIssues
? NetworkHealthState.Issue
: NetworkHealthState.Ok
)
}, [hasIssues, internetConnected])
useEffect(() => {
const onlineCallback = () => {
setSteps(initialConnectingTypeGroupState)
setInternetConnected(true)
}
const offlineCallback = () => {
setInternetConnected(false)
}
window.addEventListener('online', onlineCallback)
window.addEventListener('offline', offlineCallback)
return () => {
window.removeEventListener('online', onlineCallback)
window.removeEventListener('offline', offlineCallback)
}
}, [])
useEffect(() => {
engineCommandManager.onConnectionStateChange(
(engineConnectionState: EngineConnectionState) => {
let hasSetAStep = false
if (
engineConnectionState.type === EngineConnectionStateType.Connecting
) {
const groups = Object.values(steps)
for (let group of groups) {
for (let step of group) {
if (step[0] !== engineConnectionState.value.type) continue
step[1] = true
hasSetAStep = true
}
}
}
if (
engineConnectionState.type === EngineConnectionStateType.Disconnecting
) {
const groups = Object.values(steps)
for (let group of groups) {
for (let step of group) {
if (
engineConnectionState.value.type === DisconnectingType.Error
) {
if (
engineConnectionState.value.value.lastConnectingValue
?.type === step[0]
) {
step[1] = false
hasSetAStep = true
}
}
}
if (engineConnectionState.value.type === DisconnectingType.Error) {
setError(engineConnectionState.value.value)
}
}
}
if (hasSetAStep) {
setSteps(steps)
}
}
)
}, [])
return {
hasIssues,
overallState,
internetConnected,
steps,
issues,
error,
setHasCopied,
hasCopied,
}
}
export const NetworkHealthIndicator = () => { export const NetworkHealthIndicator = () => {
const { const {
hasIssues, hasIssues,
@ -205,7 +82,7 @@ export const NetworkHealthIndicator = () => {
error, error,
setHasCopied, setHasCopied,
hasCopied, hasCopied,
} = useNetworkStatus() } = useNetworkContext()
return ( return (
<Popover className="relative"> <Popover className="relative">
@ -259,18 +136,18 @@ export const NetworkHealthIndicator = () => {
size="lg" size="lg"
icon={ icon={
hasIssueToIcon[ hasIssueToIcon[
issues[name as ConnectingTypeGroup].toString() String(issues[name as ConnectingTypeGroup])
] ]
} }
iconClassName={ iconClassName={
hasIssueToIconColors[ hasIssueToIconColors[
issues[name as ConnectingTypeGroup].toString() String(issues[name as ConnectingTypeGroup])
].icon ].icon
} }
bgClassName={ bgClassName={
'rounded-sm ' + 'rounded-sm ' +
hasIssueToIconColors[ hasIssueToIconColors[
issues[name as ConnectingTypeGroup].toString() String(issues[name as ConnectingTypeGroup])
].bg ].bg
} }
/> />

View File

@ -5,7 +5,6 @@ import { paths } from 'lib/paths'
import { isTauri } from '../lib/isTauri' import { isTauri } from '../lib/isTauri'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { Fragment } from 'react' import { Fragment } from 'react'
import { FileTree } from './FileTree'
import { sep } from '@tauri-apps/api/path' import { sep } from '@tauri-apps/api/path'
import { Logo } from './Logo' import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'

View File

@ -4,8 +4,9 @@ import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading' import Loading from './Loading'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp' import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
import { butName } from 'lib/cameraControls' import { butName } from 'lib/cameraControls'
import { sendSelectEventToEngine } from 'lib/selections' import { sendSelectEventToEngine } from 'lib/selections'
@ -28,8 +29,11 @@ export const Stream = ({ className = '' }: { className?: string }) => {
})) }))
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const { state } = useModelingContext() const { state } = useModelingContext()
const { overallState } = useNetworkStatus() const { overallState } = useNetworkContext()
const isNetworkOkay = overallState === NetworkHealthState.Ok
const isNetworkOkay =
overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak
useEffect(() => { useEffect(() => {
if ( if (
@ -43,6 +47,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
}, [mediaStream]) }, [mediaStream])
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
if (!isNetworkOkay) return
if (!videoRef.current) return if (!videoRef.current) return
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return if (state.matches('Sketch no face')) return
@ -58,6 +63,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
} }
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
if (!isNetworkOkay) return
if (!videoRef.current) return if (!videoRef.current) return
setButtonDownInStream(undefined) setButtonDownInStream(undefined)
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
@ -72,6 +78,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
} }
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => { const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
if (!isNetworkOkay) return
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return if (state.matches('Sketch no face')) return
if (!clickCoords) return if (!clickCoords) return
@ -112,7 +119,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
{!isNetworkOkay && !isLoading && ( {!isNetworkOkay && !isLoading && (
<div className="text-center absolute inset-0"> <div className="text-center absolute inset-0">
<Loading> <Loading>
<span data-testid="loading-stream">Stream disconnected</span> <span data-testid="loading-stream">Stream disconnected...</span>
</Loading> </Loading>
</div> </div>
)} )}

View File

@ -474,19 +474,13 @@ const completionRequester = (client: LanguageServerClient) => {
} }
export const copilotPlugin = (options: LanguageServerOptions): Extension => { export const copilotPlugin = (options: LanguageServerOptions): Extension => {
let plugin: LanguageServerPlugin | null = null
return [ return [
documentUri.of(options.documentUri), documentUri.of(options.documentUri),
languageId.of('kcl'), languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders), workspaceFolders.of(options.workspaceFolders),
ViewPlugin.define( ViewPlugin.define(
(view) => (view) =>
(plugin = new LanguageServerPlugin( new LanguageServerPlugin(options.client, view, options.allowHTMLContent)
options.client,
view,
options.allowHTMLContent
))
), ),
completionDecoration, completionDecoration,
Prec.highest(completionPlugin(options.client)), Prec.highest(completionPlugin(options.client)),

View File

@ -0,0 +1,25 @@
import { createContext, useContext } from 'react'
import {
ConnectingTypeGroup,
initialConnectingTypeGroupState,
} from '../lang/std/engineConnection'
import { NetworkStatus, NetworkHealthState } from './useNetworkStatus'
export const NetworkContext = createContext<NetworkStatus>({
hasIssues: undefined,
overallState: NetworkHealthState.Disconnected,
internetConnected: true,
steps: structuredClone(initialConnectingTypeGroupState),
issues: {
[ConnectingTypeGroup.WebSocket]: undefined,
[ConnectingTypeGroup.ICE]: undefined,
[ConnectingTypeGroup.WebRTC]: undefined,
},
error: undefined,
setHasCopied: (b: boolean) => {},
hasCopied: false,
pingPongHealth: undefined,
} as NetworkStatus)
export const useNetworkContext = () => {
return useContext(NetworkContext)
}

View File

@ -0,0 +1,228 @@
import { useEffect, useState } from 'react'
import {
ConnectingType,
ConnectingTypeGroup,
DisconnectingType,
EngineCommandManagerEvents,
EngineConnectionEvents,
EngineConnectionStateType,
ErrorType,
initialConnectingTypeGroupState,
} from '../lang/std/engineConnection'
import { engineCommandManager } from '../lib/singletons'
export enum NetworkHealthState {
Ok,
Weak,
Issue,
Disconnected,
}
export interface NetworkStatus {
hasIssues: boolean | undefined
overallState: NetworkHealthState
internetConnected: boolean
steps: typeof initialConnectingTypeGroupState
issues: Record<ConnectingTypeGroup, boolean | undefined>
error: ErrorType | undefined
setHasCopied: (b: boolean) => void
hasCopied: boolean
pingPongHealth: undefined | 'OK' | 'TIMEOUT'
}
// Must be called from one place in the application.
// We've chosen the <Router /> component for this.
export function useNetworkStatus() {
const [steps, setSteps] = useState(
structuredClone(initialConnectingTypeGroupState)
)
const [internetConnected, setInternetConnected] = useState<boolean>(true)
const [overallState, setOverallState] = useState<NetworkHealthState>(
NetworkHealthState.Disconnected
)
const [pingPongHealth, setPingPongHealth] = useState<
undefined | 'OK' | 'TIMEOUT'
>(undefined)
const [hasCopied, setHasCopied] = useState<boolean>(false)
const [error, setError] = useState<ErrorType | undefined>(undefined)
const hasIssue = (i: [ConnectingType, boolean | undefined]) =>
i[1] === undefined ? i[1] : !i[1]
const [issues, setIssues] = useState<
Record<ConnectingTypeGroup, boolean | undefined>
>({
[ConnectingTypeGroup.WebSocket]: undefined,
[ConnectingTypeGroup.ICE]: undefined,
[ConnectingTypeGroup.WebRTC]: undefined,
})
const [hasIssues, setHasIssues] = useState<boolean | undefined>(undefined)
useEffect(() => {
setOverallState(
!internetConnected
? NetworkHealthState.Disconnected
: hasIssues || hasIssues === undefined
? NetworkHealthState.Issue
: pingPongHealth === 'TIMEOUT'
? NetworkHealthState.Weak
: NetworkHealthState.Ok
)
}, [hasIssues, internetConnected, pingPongHealth])
useEffect(() => {
const onlineCallback = () => {
setInternetConnected(true)
}
const offlineCallback = () => {
setInternetConnected(false)
setSteps(structuredClone(initialConnectingTypeGroupState))
}
window.addEventListener('online', onlineCallback)
window.addEventListener('offline', offlineCallback)
return () => {
window.removeEventListener('online', onlineCallback)
window.removeEventListener('offline', offlineCallback)
}
}, [])
useEffect(() => {
const issues = {
[ConnectingTypeGroup.WebSocket]: steps[
ConnectingTypeGroup.WebSocket
].reduce(
(acc: boolean | undefined, a) =>
acc === true || acc === undefined ? acc : hasIssue(a),
false
),
[ConnectingTypeGroup.ICE]: steps[ConnectingTypeGroup.ICE].reduce(
(acc: boolean | undefined, a) =>
acc === true || acc === undefined ? acc : hasIssue(a),
false
),
[ConnectingTypeGroup.WebRTC]: steps[ConnectingTypeGroup.WebRTC].reduce(
(acc: boolean | undefined, a) =>
acc === true || acc === undefined ? acc : hasIssue(a),
false
),
}
setIssues(issues)
}, [steps])
useEffect(() => {
setHasIssues(
issues[ConnectingTypeGroup.WebSocket] ||
issues[ConnectingTypeGroup.ICE] ||
issues[ConnectingTypeGroup.WebRTC]
)
}, [issues])
useEffect(() => {
const onPingPongChange = ({ detail: state }: CustomEvent) => {
setPingPongHealth(state)
}
const onConnectionStateChange = ({
detail: engineConnectionState,
}: CustomEvent) => {
setSteps((steps) => {
let nextSteps = structuredClone(steps)
if (
engineConnectionState.type === EngineConnectionStateType.Connecting
) {
const groups = Object.values(nextSteps)
for (let group of groups) {
for (let step of group) {
if (step[0] !== engineConnectionState.value.type) continue
step[1] = true
}
}
}
if (
engineConnectionState.type === EngineConnectionStateType.Disconnecting
) {
const groups = Object.values(nextSteps)
for (let group of groups) {
for (let step of group) {
if (
engineConnectionState.value.type === DisconnectingType.Error
) {
if (
engineConnectionState.value.value.lastConnectingValue
?.type === step[0]
) {
step[1] = false
}
}
}
if (engineConnectionState.value.type === DisconnectingType.Error) {
setError(engineConnectionState.value.value)
}
}
}
// Reset the state of all steps if we have disconnected.
if (
engineConnectionState.type === EngineConnectionStateType.Disconnected
) {
return structuredClone(initialConnectingTypeGroupState)
}
return nextSteps
})
}
const onEngineAvailable = ({ detail: engineConnection }: CustomEvent) => {
engineConnection.addEventListener(
EngineConnectionEvents.PingPongChanged,
onPingPongChange as EventListener
)
engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStateChanged,
onConnectionStateChange as EventListener
)
// Tell EngineConnection to start firing events.
window.dispatchEvent(new CustomEvent('use-network-status-ready', {}))
}
engineCommandManager.addEventListener(
EngineCommandManagerEvents.EngineAvailable,
onEngineAvailable as EventListener
)
return () => {
engineCommandManager.removeEventListener(
EngineCommandManagerEvents.EngineAvailable,
onEngineAvailable as EventListener
)
// When the component is unmounted these should be assigned, but it's possible
// the component mounts and unmounts before engine is available.
engineCommandManager.engineConnection?.addEventListener(
EngineConnectionEvents.PingPongChanged,
onPingPongChange as EventListener
)
engineCommandManager.engineConnection?.addEventListener(
EngineConnectionEvents.ConnectionStateChanged,
onConnectionStateChange as EventListener
)
}
}, [])
return {
hasIssues,
overallState,
internetConnected,
steps,
issues,
error,
setHasCopied,
hasCopied,
pingPongHealth,
}
}

View File

@ -43,7 +43,7 @@ export function useSetupEngineManager(
engineCommandManager.pool = settings.pool engineCommandManager.pool = settings.pool
} }
useLayoutEffect(() => { const startEngineInstance = () => {
// Load the engine command manager once with the initial width and height, // Load the engine command manager once with the initial width and height,
// then we do not want to reload it. // then we do not want to reload it.
const { width: quadWidth, height: quadHeight } = getDimensions( const { width: quadWidth, height: quadHeight } = getDimensions(
@ -73,7 +73,12 @@ export function useSetupEngineManager(
}) })
hasSetNonZeroDimensions.current = true hasSetNonZeroDimensions.current = true
} }
}, [streamRef?.current?.offsetWidth, streamRef?.current?.offsetHeight]) }
useLayoutEffect(startEngineInstance, [
streamRef?.current?.offsetWidth,
streamRef?.current?.offsetHeight,
])
useEffect(() => { useEffect(() => {
const handleResize = deferExecution(() => { const handleResize = deferExecution(() => {
@ -96,8 +101,20 @@ export function useSetupEngineManager(
} }
}, 500) }, 500)
const onOnline = () => {
startEngineInstance()
}
const onOffline = () => {
engineCommandManager.tearDown()
}
window.addEventListener('online', onOnline)
window.addEventListener('offline', onOffline)
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
return () => { return () => {
window.removeEventListener('online', onOnline)
window.removeEventListener('offline', onOffline)
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
} }
}, []) }, [])

View File

@ -7,12 +7,10 @@ import { authMachine } from 'machines/authMachine'
import { settingsMachine } from 'machines/settingsMachine' import { settingsMachine } from 'machines/settingsMachine'
import { homeMachine } from 'machines/homeMachine' import { homeMachine } from 'machines/homeMachine'
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes' import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
import {
NetworkHealthState,
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
// This might not be necessary, AnyStateMachine from xstate is working // This might not be necessary, AnyStateMachine from xstate is working
export type AllMachines = export type AllMachines =
@ -47,7 +45,7 @@ export default function useStateMachineCommands<
onCancel, onCancel,
}: UseStateMachineCommandsArgs<T, S>) { }: UseStateMachineCommandsArgs<T, S>) {
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { overallState } = useNetworkStatus() const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
const { isStreamReady } = useStore((s) => ({ const { isStreamReady } = useStore((s) => ({
isStreamReady: s.isStreamReady, isStreamReady: s.isStreamReady,
@ -55,7 +53,10 @@ export default function useStateMachineCommands<
useEffect(() => { useEffect(() => {
const disableAllButtons = const disableAllButtons =
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady (overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) ||
isExecuting ||
!isStreamReady
const newCommands = state.nextEvents const newCommands = state.nextEvents
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons) .filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n))) .filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))

File diff suppressed because it is too large Load Diff

View File

@ -318,7 +318,6 @@ function resetAndSetEngineEntitySelectionCmds(
selections: SelectionToEngine[] selections: SelectionToEngine[]
): Models['WebSocketRequest_type'][] { ): Models['WebSocketRequest_type'][] {
if (!engineCommandManager.engineConnection?.isReady()) { if (!engineCommandManager.engineConnection?.isReady()) {
console.log('engine connection is not ready')
return [] return []
} }
return [ return [

View File

@ -1880,10 +1880,10 @@
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
"@kittycad/lib@^0.0.63": "@kittycad/lib@^0.0.64":
version "0.0.63" version "0.0.64"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.63.tgz#cc70cf1c0780543bbca6f55aae40d0904cfd45d7" resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.64.tgz#0cea0788cd8af4a8964ddbf7152028affadcb17f"
integrity sha512-fDpGnycumT1xI/tSubRZzU9809/7s+m06w2EuJzxowgFrdIlvThnIHVf3EYvSujdFb0bHR/LZjodAw2ocXkXZw== integrity sha512-qHyvNYKbhsfR5aXLFrdKrBQ4JI+0G0v096oROD3HatJ+AIzg5H0THmI+rMnQ9L4zx4U6n1A9gLi7ZQjSsZsleg==
dependencies: dependencies:
node-fetch "3.3.2" node-fetch "3.3.2"
openapi-types "^12.0.0" openapi-types "^12.0.0"
@ -8234,16 +8234,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0": "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -8316,14 +8307,7 @@ string_decoder@~1.1.1:
dependencies: dependencies:
safe-buffer "~5.1.0" safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -9305,7 +9289,7 @@ workerpool@6.2.1:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -9323,15 +9307,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0: wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"