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
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
touch ./e2e/playwright/playwright-secrets.env
printf 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets.env
docker run --network host --rm --init -it playwright/chrome:playwright-1.43.1
```
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:
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)

View File

@ -1,5 +1,11 @@
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 { XOR, roundOff, uuidv4 } from 'lib/utils'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
@ -27,6 +33,8 @@ document.addEventListener('mousemove', (e) =>
)
*/
const deg = (Math.PI * 2) / 360
const commonPoints = {
startAt: '[9.06, -12.22]',
num1: 9.14,
@ -35,6 +43,8 @@ const commonPoints = {
// num2: 19.19,
}
// Utilities for writing tests that depend on test values
test.beforeEach(async ({ context, page }) => {
// wait for Vite preview server to be up
await waitOn({
@ -1484,12 +1494,15 @@ const part001 = startSketchOn('XZ')
test('Can add multiple sketches', async ({ page }) => {
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize)
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
const { toSU, click00r, expectCodeToBe } = getMovementUtils({ center, page })
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -1502,127 +1515,71 @@ test('Can add multiple sketches', async ({ page }) => {
200
)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('XZ')`
)
let codeStr = "const part001 = startSketchOn('XY')"
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
const startXPx = 600
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await click00r(0, 0)
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
await expectCodeToBe(codeStr)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await click00r(50, 0)
codeStr += ` |> line(${toSU([50, 0])}, %)`
await expectCodeToBe(codeStr)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
await click00r(0, 50)
codeStr += ` |> line(${toSU([0, 50])}, %)`
await expectCodeToBe(codeStr)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('XZ')
|> 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
await click00r(-50, 0)
codeStr += ` |> line(${toSU([-50, 0])}, %)`
await expectCodeToBe(codeStr)
// exit the sketch, reset relative clicker
click00r(undefined, undefined)
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.updateCamPosition([100, 100, 100])
await page.waitForTimeout(250)
await u.clearCommandLogs()
// start a new sketch
await u.clearCommandLogs()
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 u.clearAndCloseDebugPanel()
// 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)
codeStr += "const part002 = startSketchOn('XY')"
await expectCodeToBe(codeStr)
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 expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)`.replace(/\s/g, '')
)
await click00r(30, 0)
codeStr += ` |> startProfileAt(${toSU([30, 0])}, %)`
await expectCodeToBe(codeStr)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('${plane}')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)
|> line([0, ${roundOff(
num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
)}], %)`.replace(/\s/g, '')
)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('${plane}')
|> 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,
''
)
)
await click00r(30, 0)
codeStr += ` |> line(${toSU([30 - 0.1 /* imprecision */, 0])}, %)`
await expectCodeToBe(codeStr)
await click00r(0, 30)
codeStr += ` |> line(${toSU([0, 30])}, %)`
await expectCodeToBe(codeStr)
await click00r(-30, 0)
codeStr += ` |> line(${toSU([-30 + 0.1, 0])}, %)`
await expectCodeToBe(codeStr)
click00r(undefined, undefined)
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.updateCamPosition([100, 100, 100])
await page.waitForTimeout(250)
await u.clearCommandLogs()
})
test('ProgramMemory can be serialised', async ({ page }) => {
@ -3246,7 +3203,7 @@ test.describe('Testing segment overlays', () => {
expectAfterUnconstrained,
expectFinal,
ang = 45,
steps = 6,
steps = 10,
}: {
hoverPos: { x: number; y: number }
constraintType:
@ -3261,13 +3218,16 @@ test.describe('Testing segment overlays', () => {
steps?: number
}) => {
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(hoverPos.x, hoverPos.y, { steps })
await page.mouse.move(0, 0)
await page.waitForTimeout(1000)
let x = 0,
y = 0
x = hoverPos.x + Math.cos(ang * deg) * 32
y = hoverPos.y - Math.sin(ang * deg) * 32
await page.mouse.move(x, y)
await wiggleMove(page, x, y, 20, 30, ang, 10, 5)
await expect(page.locator('.cm-content')).toContainText(
expectBeforeUnconstrained
)
@ -3283,6 +3243,14 @@ test.describe('Testing segment overlays', () => {
await expect(page.locator('.cm-content')).toContainText(
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(
`[data-constraint-type="${constraintType}"][data-is-constrained="false"]`
)
@ -3328,14 +3296,16 @@ test.describe('Testing segment overlays', () => {
ang?: number
steps?: number
}) => {
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.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.getByText('Added variable')).not.toBeVisible()
await page.mouse.move(hoverPos.x, hoverPos.y, { steps })
await expect(page.locator('.cm-content')).toContainText(
expectBeforeUnconstrained
)
@ -3353,7 +3323,14 @@ test.describe('Testing segment overlays', () => {
expectAfterUnconstrained
)
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(
`[data-constraint-type="${constraintType}"][data-is-constrained="true"]`
)
@ -3365,6 +3342,7 @@ test.describe('Testing segment overlays', () => {
await constrainedLocator.click()
await expect(page.locator('.cm-content')).toContainText(expectFinal)
}
test.setTimeout(120000)
test('for segments [line, angledLine, lineTo, xLineTo]', async ({
page,
}) => {
@ -3372,24 +3350,24 @@ test.describe('Testing segment overlays', () => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> startProfileAt([5 + 0, 20 + 0], %)
|> line([0.5, -14 + 0], %)
|> angledLine({ angle: 3 + 0, length: 32 + 0 }, %)
|> lineTo([33, 11.5 + 0], %)
|> xLineTo(9 - 5, %)
|> yLineTo(-10.77, %, 'a')
|> lineTo([5 + 33, 20 + 11.5 + 0], %)
|> xLineTo(5 + 9 - 5, %)
|> yLineTo(20 + -10.77, %, 'a')
|> xLine(26.04, %)
|> yLine(21.14 + 0, %)
|> angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %)
|> angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)
|> angledLineToX({ angle: 3 + 0, to: 26 }, %)
|> angledLineToY({ angle: 89, to: 9.14 + 0 }, %)
|> angledLineToX({ angle: 3 + 0, to: 5 + 26 }, %)
|> angledLineToY({ angle: 89, to: 20 + 9.14 + 0 }, %)
|> angledLineThatIntersects({
angle: 4.14,
intersectTag: 'a',
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 u.closeDebugPanel()
await page.getByText('xLineTo(9 - 5, %)').click()
await page.getByText('xLineTo(5 + 9 - 5, %)').click()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500)
@ -3434,45 +3413,62 @@ test.describe('Testing segment overlays', () => {
const clickUnconstrained = _clickUnconstrained(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}"]`)
console.log('line1')
ang = await u.getAngle(`[data-overlay-index="${0}"]`)
console.log('line1', line, ang)
await clickConstrained({
hoverPos: { x: line.x, y: line.y - 10 },
hoverPos: { x: line.x, y: line.y },
constraintType: 'yRelative',
expectBeforeUnconstrained: '|> line([0.5, -14 + 0], %)',
expectAfterUnconstrained: '|> line([0.5, -14], %)',
expectFinal: '|> line([0.5, yRel001], %)',
ang: 135,
ang: ang + 180,
})
console.log('line2')
await clickUnconstrained({
hoverPos: { x: line.x, y: line.y - 10 },
hoverPos: { x: line.x, y: line.y },
constraintType: 'xRelative',
expectBeforeUnconstrained: '|> line([0.5, yRel001], %)',
expectAfterUnconstrained: 'line([xRel001, yRel001], %)',
expectFinal: '|> line([0.5, yRel001], %)',
ang: -45,
ang: ang + 180,
})
const angledLine = await u.getBoundingBox(`[data-overlay-index="1"]`)
ang = await u.getAngle(`[data-overlay-index="1"]`)
console.log('angledLine1')
await clickConstrained({
hoverPos: { x: angledLine.x - 10, y: angledLine.y },
hoverPos: { x: angledLine.x, y: angledLine.y },
constraintType: 'angle',
expectBeforeUnconstrained:
'angledLine({ angle: 3 + 0, length: 32 + 0 }, %)',
expectAfterUnconstrained: 'angledLine({ angle: 3, length: 32 + 0 }, %)',
expectFinal: 'angledLine({ angle: angle001, length: 32 + 0 }, %)',
ang: ang + 180,
})
console.log('angledLine2')
await clickConstrained({
hoverPos: { x: angledLine.x - 10, y: angledLine.y },
hoverPos: { x: angledLine.x, y: angledLine.y },
constraintType: 'length',
expectBeforeUnconstrained:
'angledLine({ angle: angle001, length: 32 + 0 }, %)',
expectAfterUnconstrained:
'angledLine({ angle: angle001, length: 32 }, %)',
expectFinal: 'angledLine({ angle: angle001, length: len001 }, %)',
ang: ang + 180,
})
await page.mouse.move(700, 250)
@ -3482,36 +3478,39 @@ test.describe('Testing segment overlays', () => {
}
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')
await clickConstrained({
hoverPos: { x: lineTo.x, y: lineTo.y + 21 },
hoverPos: { x: lineTo.x, y: lineTo.y },
constraintType: 'yAbsolute',
expectBeforeUnconstrained: 'lineTo([33, 11.5 + 0], %)',
expectAfterUnconstrained: 'lineTo([33, 11.5], %)',
expectFinal: 'lineTo([33, yAbs001], %)',
expectBeforeUnconstrained: 'lineTo([5 + 33, 20 + 11.5 + 0], %)',
expectAfterUnconstrained: 'lineTo([5 + 33, 31.5], %)',
expectFinal: 'lineTo([5 + 33, yAbs001], %)',
steps: 8,
ang: 55,
ang: ang + 180,
})
console.log('lineTo2')
await clickUnconstrained({
hoverPos: { x: lineTo.x, y: lineTo.y + 25 },
await clickConstrained({
hoverPos: { x: lineTo.x, y: lineTo.y },
constraintType: 'xAbsolute',
expectBeforeUnconstrained: 'lineTo([33, yAbs001], %)',
expectAfterUnconstrained: 'lineTo([xAbs001, yAbs001], %)',
expectFinal: 'lineTo([33, yAbs001], %)',
expectBeforeUnconstrained: 'lineTo([5 + 33, yAbs001], %)',
expectAfterUnconstrained: 'lineTo([38, yAbs001], %)',
expectFinal: 'lineTo([xAbs001, yAbs001], %)',
steps: 8,
ang: ang + 180,
})
const xLineTo = await u.getBoundingBox(`[data-overlay-index="3"]`)
ang = await u.getAngle(`[data-overlay-index="3"]`)
console.log('xlineTo1')
await clickConstrained({
hoverPos: { x: xLineTo.x + 15, y: xLineTo.y },
hoverPos: { x: xLineTo.x, y: xLineTo.y },
constraintType: 'xAbsolute',
expectBeforeUnconstrained: 'xLineTo(9 - 5, %)',
expectAfterUnconstrained: 'xLineTo(4, %)',
expectBeforeUnconstrained: 'xLineTo(5 + 9 - 5, %)',
expectAfterUnconstrained: 'xLineTo(9, %)',
expectFinal: 'xLineTo(xAbs002, %)',
ang: -45,
ang: ang + 180,
steps: 8,
})
})
@ -3566,26 +3565,31 @@ const part001 = startSketchOn('XZ')
await page.waitForTimeout(300)
let ang = 0
const yLineTo = await u.getBoundingBox(`[data-overlay-index="4"]`)
ang = await u.getAngle(`[data-overlay-index="4"]`)
console.log('ylineTo1')
await clickUnconstrained({
hoverPos: { x: yLineTo.x, y: yLineTo.y - 30 },
hoverPos: { x: yLineTo.x, y: yLineTo.y },
constraintType: 'yAbsolute',
expectBeforeUnconstrained: "yLineTo(-10.77, %, 'a')",
expectAfterUnconstrained: "yLineTo(yAbs002, %, 'a')",
expectFinal: "yLineTo(-10.77, %, 'a')",
ang: ang + 180,
})
const xLine = await u.getBoundingBox(`[data-overlay-index="5"]`)
ang = await u.getAngle(`[data-overlay-index="5"]`)
console.log('xline')
await clickUnconstrained({
hoverPos: { x: xLine.x - 25, y: xLine.y },
hoverPos: { x: xLine.x, y: xLine.y },
constraintType: 'xRelative',
expectBeforeUnconstrained: 'xLine(26.04, %)',
expectAfterUnconstrained: 'xLine(xRel002, %)',
expectFinal: 'xLine(26.04, %)',
steps: 10,
ang: 50,
ang: ang + 180,
})
})
test('for segments [yLine, angledLineOfXLength, angledLineOfYLength]', async ({
@ -3625,6 +3629,7 @@ const part001 = startSketchOn('XZ')
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await page.waitForTimeout(500)
await page.getByText('xLineTo(9 - 5, %)').click()
await page.waitForTimeout(100)
@ -3636,7 +3641,10 @@ const part001 = startSketchOn('XZ')
const clickUnconstrained = _clickUnconstrained(page)
const clickConstrained = _clickConstrained(page)
let ang = 0
const yLine = await u.getBoundingBox(`[data-overlay-index="6"]`)
ang = await u.getAngle(`[data-overlay-index="6"]`)
console.log('yline1')
await clickConstrained({
hoverPos: { x: yLine.x, y: yLine.y + 20 },
@ -3644,11 +3652,13 @@ const part001 = startSketchOn('XZ')
expectBeforeUnconstrained: 'yLine(21.14 + 0, %)',
expectAfterUnconstrained: 'yLine(21.14, %)',
expectFinal: 'yLine(yRel001, %)',
ang: ang + 180,
})
const angledLineOfXLength = await u.getBoundingBox(
`[data-overlay-index="7"]`
)
ang = await u.getAngle(`[data-overlay-index="7"]`)
console.log('angledLineOfXLength1')
await clickConstrained({
hoverPos: { x: angledLineOfXLength.x + 20, y: angledLineOfXLength.y },
@ -3659,6 +3669,7 @@ const part001 = startSketchOn('XZ')
'angledLineOfXLength({ angle: -179, length: 23.14 }, %)',
expectFinal:
'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)',
ang: ang + 180,
})
console.log('angledLineOfXLength2')
await clickUnconstrained({
@ -3671,11 +3682,13 @@ const part001 = startSketchOn('XZ')
expectFinal:
'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)',
steps: 7,
ang: ang + 180,
})
const angledLineOfYLength = await u.getBoundingBox(
`[data-overlay-index="8"]`
)
ang = await u.getAngle(`[data-overlay-index="8"]`)
console.log('angledLineOfYLength1')
await clickUnconstrained({
hoverPos: { x: angledLineOfYLength.x, y: angledLineOfYLength.y - 20 },
@ -3685,7 +3698,7 @@ const part001 = startSketchOn('XZ')
expectAfterUnconstrained:
'angledLineOfYLength({ angle: angle002, length: 19 + 0 }, %)',
expectFinal: 'angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)',
ang: 135,
ang: ang + 180,
steps: 6,
})
console.log('angledLineOfYLength2')
@ -3697,14 +3710,13 @@ const part001 = startSketchOn('XZ')
expectAfterUnconstrained:
'angledLineOfYLength({ angle: -91, length: 19 }, %)',
expectFinal: 'angledLineOfYLength({ angle: -91, length: yRel002 }, %)',
ang: -45,
ang: ang + 180,
steps: 7,
})
})
test('for segments [angledLineToX, angledLineToY, angledLineThatIntersects]', async ({
page,
}) => {
test.skip(process.platform !== 'darwin', 'too flakey on ubuntu')
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -3750,14 +3762,18 @@ const part001 = startSketchOn('XZ')
const clickUnconstrained = _clickUnconstrained(page)
const clickConstrained = _clickConstrained(page)
let ang = 0
const angledLineToX = await u.getBoundingBox(`[data-overlay-index="9"]`)
ang = await u.getAngle(`[data-overlay-index="9"]`)
console.log('angledLineToX')
await clickConstrained({
hoverPos: { x: angledLineToX.x - 20, y: angledLineToX.y },
hoverPos: { x: angledLineToX.x, y: angledLineToX.y },
constraintType: 'angle',
expectBeforeUnconstrained: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)',
expectAfterUnconstrained: 'angledLineToX({ angle: 3, to: 26 }, %)',
expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)',
ang: ang + 180,
})
console.log('angledLineToX2')
await clickUnconstrained({
@ -3768,12 +3784,14 @@ const part001 = startSketchOn('XZ')
expectAfterUnconstrained:
'angledLineToX({ angle: angle001, to: xAbs001 }, %)',
expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)',
ang: ang + 180,
})
const angledLineToY = await u.getBoundingBox(`[data-overlay-index="10"]`)
ang = await u.getAngle(`[data-overlay-index="10"]`)
console.log('angledLineToY')
await clickUnconstrained({
hoverPos: { x: angledLineToY.x, y: angledLineToY.y + 20 },
hoverPos: { x: angledLineToY.x, y: angledLineToY.y },
constraintType: 'angle',
expectBeforeUnconstrained:
'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
@ -3781,7 +3799,7 @@ const part001 = startSketchOn('XZ')
'angledLineToY({ angle: angle002, to: 9.14 + 0 }, %)',
expectFinal: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
steps: process.platform === 'darwin' ? 8 : 9,
ang: 135,
ang: ang + 180,
})
console.log('angledLineToY2')
await clickConstrained({
@ -3791,12 +3809,13 @@ const part001 = startSketchOn('XZ')
'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
expectAfterUnconstrained: 'angledLineToY({ angle: 89, to: 9.14 }, %)',
expectFinal: 'angledLineToY({ angle: 89, to: yAbs001 }, %)',
ang: 135,
ang: ang + 180,
})
const angledLineThatIntersects = await u.getBoundingBox(
`[data-overlay-index="11"]`
)
ang = await u.getAngle(`[data-overlay-index="11"]`)
console.log('angledLineThatIntersects')
await clickUnconstrained({
hoverPos: {
@ -3819,7 +3838,7 @@ const part001 = startSketchOn('XZ')
offset: 9,
intersectTag: 'a'
}, %)`,
ang: -45,
ang: ang + 180,
})
console.log('angledLineThatIntersects2')
await clickUnconstrained({
@ -3843,7 +3862,7 @@ const part001 = startSketchOn('XZ')
offset: 9,
intersectTag: 'a'
}, %)`,
ang: -25,
ang: ang + 180,
})
})
test('for segment [tangentialArcTo]', async ({ page }) => {
@ -3895,24 +3914,25 @@ const part001 = startSketchOn('XZ')
const tangentialArcTo = await u.getBoundingBox(
`[data-overlay-index="12"]`
)
let ang = await u.getAngle(`[data-overlay-index="12"]`)
console.log('tangentialArcTo')
await clickConstrained({
hoverPos: { x: tangentialArcTo.x - 10, y: tangentialArcTo.y + 20 },
hoverPos: { x: tangentialArcTo.x, y: tangentialArcTo.y },
constraintType: 'xAbsolute',
expectBeforeUnconstrained: 'tangentialArcTo([3.14 + 13, -3.14], %)',
expectAfterUnconstrained: 'tangentialArcTo([16.14, -3.14], %)',
expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)',
ang: -45,
ang: ang + 180,
steps: 6,
})
console.log('tangentialArcTo2')
await clickUnconstrained({
hoverPos: { x: tangentialArcTo.x - 10, y: tangentialArcTo.y + 20 },
hoverPos: { x: tangentialArcTo.x, y: tangentialArcTo.y },
constraintType: 'yAbsolute',
expectBeforeUnconstrained: 'tangentialArcTo([xAbs001, -3.14], %)',
expectAfterUnconstrained: 'tangentialArcTo([xAbs001, yAbs001], %)',
expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)',
ang: -135,
ang: ang + 180,
steps: 10,
})
})
@ -4007,25 +4027,7 @@ const part001 = startSketchOn('XZ')
steps: 6,
})
segmentToDelete = await getOverlayByIndex(0)
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)
segmentToDelete = await getOverlayByIndex(11)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
codeToBeDeleted: `angledLineThatIntersects({
@ -4038,21 +4040,21 @@ const part001 = startSketchOn('XZ')
steps: 7,
})
segmentToDelete = await getOverlayByIndex(8)
segmentToDelete = await getOverlayByIndex(10)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
codeToBeDeleted: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
stdLibFnName: 'angledLineToY',
})
segmentToDelete = await getOverlayByIndex(7)
segmentToDelete = await getOverlayByIndex(9)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y },
codeToBeDeleted: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)',
stdLibFnName: 'angledLineToX',
})
segmentToDelete = await getOverlayByIndex(6)
segmentToDelete = await getOverlayByIndex(8)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 },
codeToBeDeleted:
@ -4060,7 +4062,7 @@ const part001 = startSketchOn('XZ')
stdLibFnName: 'angledLineOfYLength',
})
segmentToDelete = await getOverlayByIndex(5)
segmentToDelete = await getOverlayByIndex(7)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
codeToBeDeleted:
@ -4068,42 +4070,36 @@ const part001 = startSketchOn('XZ')
stdLibFnName: 'angledLineOfXLength',
})
segmentToDelete = await getOverlayByIndex(4)
segmentToDelete = await getOverlayByIndex(6)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y + 10 },
codeToBeDeleted: 'yLine(21.14 + 0, %)',
stdLibFnName: 'yLine',
})
segmentToDelete = await getOverlayByIndex(3)
segmentToDelete = await getOverlayByIndex(5)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y },
codeToBeDeleted: 'xLine(26.04, %)',
stdLibFnName: 'xLine',
})
segmentToDelete = await getOverlayByIndex(2)
segmentToDelete = await getOverlayByIndex(4)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 },
codeToBeDeleted: "yLineTo(-10.77, %, 'a')",
stdLibFnName: 'yLineTo',
})
segmentToDelete = await getOverlayByIndex(1)
segmentToDelete = await getOverlayByIndex(3)
await deleteSegmentSequence({
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
codeToBeDeleted: 'xLineTo(9 - 5, %)',
stdLibFnName: 'xLineTo',
})
for (let i = 0; i < 15; i++) {
await page.mouse.wheel(0, 100)
await page.waitForTimeout(25)
}
await page.waitForTimeout(200)
segmentToDelete = await getOverlayByIndex(0)
// Not sure why this is diff. from the others - Kurt, ideas?
segmentToDelete = await getOverlayByIndex(2)
const hoverPos = { x: segmentToDelete.x - 10, y: segmentToDelete.y + 10 }
await expect(page.getByText('Added variable')).not.toBeVisible()
const [x, y] = [
@ -4122,6 +4118,24 @@ const part001 = startSketchOn('XZ')
await expect(page.locator('.cm-content')).not.toContainText(
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', () => {
@ -4541,6 +4555,11 @@ test('simulate network down and network little widget', async ({ page }) => {
await page.goto('/')
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"]')
await expect(networkWidget).toBeVisible()
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"]')
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()
// Click the network widget
@ -4580,7 +4599,7 @@ test('simulate network down and network little widget', async ({ page }) => {
await expect(networkPopover).toBeVisible()
// Click off the modal.
await page.mouse.click(100, 100)
await page.mouse.click(0, 0)
await expect(networkPopover).not.toBeVisible()
// Turn back on the network
@ -4592,7 +4611,11 @@ test('simulate network down and network little widget', async ({ page }) => {
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()
})
@ -4606,8 +4629,7 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
).not.toBeDisabled({ timeout: 15000 })
// click on "Start Sketch" button
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
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// Expect the network to be up
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')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([-11.59, 11.1], %)`)
|> line([-11.64, 11.11], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([-11.59, 11.1], %)
|> line([-6.61, 0], %)`)
|> line([-11.64, 11.11], %)
|> line([-6.56, 0], %)`)
// Unequip line tool
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()
}
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) {
// Chrome devtools protocol session only works in Chromium
const browserType = page.context().browser()?.browserType().name()
@ -145,11 +218,15 @@ export async function getUtils(page: Page) {
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) =>
page
.locator(locator)
.boundingBox()
.then((box) => ({ x: box?.x || 0, y: box?.y || 0 })),
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
doAndWaitForCmd: async (
fn: () => Promise<void>,
commandType: string,

View File

@ -10,7 +10,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.63",
"@kittycad/lib": "^0.0.64",
"@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^2.0.1",
@ -95,7 +95,8 @@
"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",
"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": {
"trailingComma": "es5",

View File

@ -12,12 +12,12 @@ import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e/playwright',
/* 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. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
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,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
@ -72,7 +72,7 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: 'yarn serve',
command: 'yarn start',
// url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},

View File

@ -12,6 +12,8 @@ import SignIn from './routes/SignIn'
import { Auth } from './Auth'
import { isTauri } from './lib/isTauri'
import Home from './routes/Home'
import { NetworkContext } from './hooks/useNetworkContext'
import { useNetworkStatus } from './hooks/useNetworkStatus'
import makeUrlPathRelative from './lib/makeUrlPathRelative'
import DownloadAppBanner from 'components/DownloadAppBanner'
import { WasmErrBanner } from 'components/WasmErrBanner'
@ -155,5 +157,11 @@ const router = createBrowserRouter([
* @returns RouterProvider
*/
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 { useModelingContext } from 'hooks/useModelingContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ActionButton } from 'components/ActionButton'
import { isSingleCursorInPipe } from 'lang/queryAst'
import { useKclContext } from 'lang/KclProvider'
import {
NetworkHealthState,
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { useStore } from 'useStore'
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
import { useHotkeys } from 'react-hotkeys-hook'
@ -38,14 +36,16 @@ export function Toolbar({
}, [engineCommandManager.artifactMap, context.selectionRanges])
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
const { overallState } = useNetworkStatus()
const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext()
const { isStreamReady } = useStore((s) => ({
isStreamReady: s.isStreamReady,
}))
const disableAllButtons =
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
(overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) ||
isExecuting ||
!isStreamReady
useHotkeys(
'l',

View File

@ -1,14 +1,65 @@
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 [hasLongLoadTime, setHasLongLoadTime] = useState(false)
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
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(() => {
setHasLongLoadTime(true)
setError(ConnectionError.LongLoadingTime)
}, 4000)
return () => clearTimeout(timer)
}, [setHasLongLoadTime])
}, [error, setError])
return (
<div
className="body-bg flex flex-col items-center justify-center h-screen"
@ -29,10 +80,10 @@ const Loading = ({ children }: React.PropsWithChildren) => {
<p
className={
'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>
</div>
)

View File

@ -13,7 +13,6 @@ import { LanguageSupport } from '@codemirror/language'
import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths'
import { FileEntry } from 'lib/types'
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
import Worker from 'editor/plugins/lsp/worker.ts?worker'
import {
LspWorkerEventType,
@ -23,6 +22,8 @@ import {
} from 'editor/plugins/lsp/types'
import { wasmUrl } from 'lang/wasm'
import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return []
@ -86,7 +87,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
} = useSettingsAuthContext()
const token = auth?.context.token
const navigate = useNavigate()
const { overallState } = useNetworkStatus()
const { overallState } = useNetworkContext()
const isNetworkOkay = overallState === NetworkHealthState.Ok
// 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 {
NETWORK_HEALTH_TEXT,
NetworkHealthIndicator,
NetworkHealthState,
} from './NetworkHealthIndicator'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
function TestWrap({ children }: { children: React.ReactNode }) {
// 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', () => {
test('Renders the network indicator', () => {
render(
@ -29,21 +30,7 @@ describe('NetworkHealthIndicator tests', () => {
fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network')).toHaveTextContent(
NETWORK_HEALTH_TEXT[NetworkHealthState.Ok]
)
})
test('Responds to network changes', () => {
render(
<TestWrap>
<NetworkHealthIndicator />
</TestWrap>
)
fireEvent.offline(window)
fireEvent.click(screen.getByTestId('network-toggle'))
// Starts as disconnected
expect(screen.getByTestId('network')).toHaveTextContent(
NETWORK_HEALTH_TEXT[NetworkHealthState.Disconnected]
)

View File

@ -1,26 +1,13 @@
import { Popover } from '@headlessui/react'
import { useEffect, useState } from 'react'
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'
export enum NetworkHealthState {
Ok,
Issue,
Disconnected,
}
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
import { useNetworkContext } from '../hooks/useNetworkContext'
import { NetworkHealthState } from '../hooks/useNetworkStatus'
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
[NetworkHealthState.Ok]: 'Connected',
[NetworkHealthState.Weak]: 'Weak',
[NetworkHealthState.Issue]: 'Problem',
[NetworkHealthState.Disconnected]: 'Offline',
}
@ -61,6 +48,10 @@ const overallConnectionStateColor: Record<NetworkHealthState, IconColorConfig> =
icon: 'text-succeed-80 dark:text-succeed-10',
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]: {
icon: 'text-destroy-80 dark:text-destroy-10',
bg: 'bg-destroy-10 dark:bg-destroy-80/80',
@ -76,125 +67,11 @@ const overallConnectionStateIcon: Record<
ActionIconProps['icon']
> = {
[NetworkHealthState.Ok]: 'network',
[NetworkHealthState.Weak]: 'network',
[NetworkHealthState.Issue]: '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 = () => {
const {
hasIssues,
@ -205,7 +82,7 @@ export const NetworkHealthIndicator = () => {
error,
setHasCopied,
hasCopied,
} = useNetworkStatus()
} = useNetworkContext()
return (
<Popover className="relative">
@ -259,18 +136,18 @@ export const NetworkHealthIndicator = () => {
size="lg"
icon={
hasIssueToIcon[
issues[name as ConnectingTypeGroup].toString()
String(issues[name as ConnectingTypeGroup])
]
}
iconClassName={
hasIssueToIconColors[
issues[name as ConnectingTypeGroup].toString()
String(issues[name as ConnectingTypeGroup])
].icon
}
bgClassName={
'rounded-sm ' +
hasIssueToIconColors[
issues[name as ConnectingTypeGroup].toString()
String(issues[name as ConnectingTypeGroup])
].bg
}
/>

View File

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

View File

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

View File

@ -474,19 +474,13 @@ const completionRequester = (client: LanguageServerClient) => {
}
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
let plugin: LanguageServerPlugin | null = null
return [
documentUri.of(options.documentUri),
languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders),
ViewPlugin.define(
(view) =>
(plugin = new LanguageServerPlugin(
options.client,
view,
options.allowHTMLContent
))
new LanguageServerPlugin(options.client, view, options.allowHTMLContent)
),
completionDecoration,
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
}
useLayoutEffect(() => {
const startEngineInstance = () => {
// Load the engine command manager once with the initial width and height,
// then we do not want to reload it.
const { width: quadWidth, height: quadHeight } = getDimensions(
@ -73,7 +73,12 @@ export function useSetupEngineManager(
})
hasSetNonZeroDimensions.current = true
}
}, [streamRef?.current?.offsetWidth, streamRef?.current?.offsetHeight])
}
useLayoutEffect(startEngineInstance, [
streamRef?.current?.offsetWidth,
streamRef?.current?.offsetHeight,
])
useEffect(() => {
const handleResize = deferExecution(() => {
@ -96,8 +101,20 @@ export function useSetupEngineManager(
}
}, 500)
const onOnline = () => {
startEngineInstance()
}
const onOffline = () => {
engineCommandManager.tearDown()
}
window.addEventListener('online', onOnline)
window.addEventListener('offline', onOffline)
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('online', onOnline)
window.removeEventListener('offline', onOffline)
window.removeEventListener('resize', handleResize)
}
}, [])

View File

@ -7,12 +7,10 @@ import { authMachine } from 'machines/authMachine'
import { settingsMachine } from 'machines/settingsMachine'
import { homeMachine } from 'machines/homeMachine'
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
import {
NetworkHealthState,
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { useKclContext } from 'lang/KclProvider'
import { useStore } from 'useStore'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
// This might not be necessary, AnyStateMachine from xstate is working
export type AllMachines =
@ -47,7 +45,7 @@ export default function useStateMachineCommands<
onCancel,
}: UseStateMachineCommandsArgs<T, S>) {
const { commandBarSend } = useCommandsContext()
const { overallState } = useNetworkStatus()
const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext()
const { isStreamReady } = useStore((s) => ({
isStreamReady: s.isStreamReady,
@ -55,7 +53,10 @@ export default function useStateMachineCommands<
useEffect(() => {
const disableAllButtons =
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
(overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) ||
isExecuting ||
!isStreamReady
const newCommands = state.nextEvents
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
.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[]
): Models['WebSocketRequest_type'][] {
if (!engineCommandManager.engineConnection?.isReady()) {
console.log('engine connection is not ready')
return []
}
return [

View File

@ -1880,10 +1880,10 @@
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
"@kittycad/lib@^0.0.63":
version "0.0.63"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.63.tgz#cc70cf1c0780543bbca6f55aae40d0904cfd45d7"
integrity sha512-fDpGnycumT1xI/tSubRZzU9809/7s+m06w2EuJzxowgFrdIlvThnIHVf3EYvSujdFb0bHR/LZjodAw2ocXkXZw==
"@kittycad/lib@^0.0.64":
version "0.0.64"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.64.tgz#0cea0788cd8af4a8964ddbf7152028affadcb17f"
integrity sha512-qHyvNYKbhsfR5aXLFrdKrBQ4JI+0G0v096oROD3HatJ+AIzg5H0THmI+rMnQ9L4zx4U6n1A9gLi7ZQjSsZsleg==
dependencies:
node-fetch "3.3.2"
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"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0":
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:
"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==
@ -8316,14 +8307,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm: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:
"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==
@ -9305,7 +9289,7 @@ workerpool@6.2.1:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
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"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -9323,15 +9307,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.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:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"