Compare commits
4 Commits
kurt-zoom-
...
watch-fs
Author | SHA1 | Date | |
---|---|---|---|
dcbfccc621 | |||
3035ad16fc | |||
74faf0461c | |||
47e472e984 |
@ -343,7 +343,12 @@ jobs:
|
||||
with:
|
||||
files: 'out/Zoo*'
|
||||
|
||||
# TODO: Add GitHub publisher
|
||||
- name: Invalidate bucket cache on latest*.yml and last_download.json files
|
||||
run: |
|
||||
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/last_download.json" --async
|
||||
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-linux-arm64.yml" --async
|
||||
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async
|
||||
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async
|
||||
|
||||
announce_release:
|
||||
needs: [publish-apps-release]
|
||||
|
64
README.md
64
README.md
@ -322,6 +322,70 @@ cd src/wasm-lib
|
||||
cargo test
|
||||
```
|
||||
|
||||
### Mapping CI CD jobs to local commands
|
||||
|
||||
When you see the CI CD fail on jobs you may wonder three things
|
||||
- Do I have a bug in my code?
|
||||
- Is the test flaky?
|
||||
- Is there a bug in `main`?
|
||||
|
||||
To answer these questions the following commands will give you confidence to locate the issue.
|
||||
|
||||
#### Static Analysis
|
||||
|
||||
Part of the CI CD pipeline performs static analysis on the code. Use the following commands to mimic the CI CD jobs.
|
||||
|
||||
The following set of commands should get us closer to one and done commands to instantly retest issues.
|
||||
|
||||
```
|
||||
yarn test-setup
|
||||
```
|
||||
|
||||
> Gotcha, are packages up to date and is the wasm built?
|
||||
|
||||
|
||||
```
|
||||
yarn tsc
|
||||
yarn fmt-check
|
||||
yarn lint
|
||||
yarn xstate:typegen
|
||||
yarn test:unit:local
|
||||
```
|
||||
|
||||
> Gotcha: Our unit tests have integration tests in them. You need to run a localhost server to run the unit tests.
|
||||
|
||||
#### E2E Tests
|
||||
|
||||
**Playwright Browser**
|
||||
|
||||
These E2E tests run in a browser (without electron).
|
||||
There are tests that are skipped if they are ran in a windows OS or Linux OS. We can use playwright tags to implement test skipping.
|
||||
|
||||
Breaking down the command `yarn test:playwright:browser:chrome:windows`
|
||||
- The application is `playwright`
|
||||
- The runtime is a `browser`
|
||||
- The specific `browser` is `chrome`
|
||||
- The test should run in a `windows` environment. It will skip tests that are broken or flaky in the windows OS.
|
||||
|
||||
```
|
||||
yarn test:playwright:browser:chrome
|
||||
yarn test:playwright:browser:chrome:windows
|
||||
yarn test:playwright:browser:chrome:ubuntu
|
||||
```
|
||||
|
||||
**Playwright Electron**
|
||||
|
||||
These E2E tests run in electron. There are tests that are skipped if they are ran in a windows, linux, or macos environment. We can use playwright tags to implement test skipping.
|
||||
|
||||
```
|
||||
yarn test:playwright:electron:local
|
||||
yarn test:playwright:electron:windows:local
|
||||
yarn test:playwright:electron:macos:local
|
||||
yarn test:playwright:electron:ubuntu:local
|
||||
```
|
||||
|
||||
> Why does it say local? The CI CD commands that run in the pipeline cannot be ran locally. A single command will not properly setup the testing environment locally.
|
||||
|
||||
#### Some notes on CI
|
||||
|
||||
The tests are broken into snapshot tests and non-snapshot tests, and they run in that order, they automatically commit new snap shots, so if you see an image commit check it was an intended change. If we have non-determinism in the snapshots such that they are always committing new images, hopefully this annoyance makes us fix them asap, if you notice this happening let Kurt know. But for the odd occasion `git reset --hard HEAD~ && git push -f` is your friend.
|
||||
|
@ -154,7 +154,7 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
||||
}
|
||||
|
||||
test.describe('Basic sketch', () => {
|
||||
test('code pane open at start', async ({ page }) => {
|
||||
test('code pane open at start', { tag: ['@skipWin'] }, async ({ page }) => {
|
||||
// Skip on windows it is being weird.
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
await doBasicSketch(page, ['code'])
|
||||
|
@ -72,7 +72,7 @@ export class EditorFixture {
|
||||
const content = await this.diagnosticsTooltip.allTextContents()
|
||||
diagnosticsContent.push(content.join(''))
|
||||
}
|
||||
return [...new Set(diagnosticsContent)].map((d) => d.trim())
|
||||
return [...new Set(diagnosticsContent)].map((d) => sansWhitespace(d))
|
||||
}
|
||||
|
||||
private _getHighlightedCode = async () => {
|
||||
@ -108,4 +108,11 @@ export class EditorFixture {
|
||||
diagnostics: expectedState.diagnostics.map(sansWhitespace),
|
||||
})
|
||||
}
|
||||
replaceCode = async (findCode: string, replaceCode: string) => {
|
||||
const lines = await this.page.locator('.cm-line').all()
|
||||
let code = (await Promise.all(lines.map((c) => c.textContent()))).join('\n')
|
||||
if (!lines) return
|
||||
code = code.replace(findCode, replaceCode)
|
||||
await this.codeContent.fill(code)
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { uuidv4 } from 'lib/utils'
|
||||
import {
|
||||
closeDebugPanel,
|
||||
doAndWaitForImageDiff,
|
||||
getPixelRGBs,
|
||||
openAndClearDebugPanel,
|
||||
sendCustomCmd,
|
||||
} from '../test-utils'
|
||||
@ -89,4 +90,28 @@ export class SceneFixture {
|
||||
waitForExecutionDone = async () => {
|
||||
await expect(this.exeIndicator).toBeVisible()
|
||||
}
|
||||
|
||||
expectPixelColor = async (
|
||||
colour: [number, number, number],
|
||||
coords: { x: number; y: number },
|
||||
diff: number
|
||||
) => {
|
||||
let finalValue = colour
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const pixel = (await getPixelRGBs(this.page)(coords, 1))[0]
|
||||
if (!pixel) return null
|
||||
finalValue = pixel
|
||||
return pixel.every(
|
||||
(channel, index) => Math.abs(channel - colour[index]) < diff
|
||||
)
|
||||
})
|
||||
.toBeTruthy()
|
||||
.catch((cause) => {
|
||||
throw new Error(
|
||||
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
|
||||
{ cause }
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -5,13 +5,10 @@ import { ToolbarFixture } from './fixtures/toolbarFixture'
|
||||
|
||||
// test file is for testing point an click code gen functionality that's not sketch mode related
|
||||
|
||||
test('verify extruding circle works', async ({
|
||||
app,
|
||||
cmdBar,
|
||||
editor,
|
||||
toolbar,
|
||||
scene,
|
||||
}) => {
|
||||
test(
|
||||
'verify extruding circle works',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ app, cmdBar, editor, toolbar, scene }) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'Fails on windows in CI, can not be replicated locally on windows.'
|
||||
@ -69,7 +66,8 @@ test('verify extruding circle works', async ({
|
||||
|
||||
await editor.expectEditor.toContain(expectString)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.describe('verify sketch on chamfer works', () => {
|
||||
const _sketchOnAChamfer =
|
||||
@ -152,12 +150,10 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
})
|
||||
})
|
||||
}
|
||||
test('works on all edge selections and can break up multi edges in a chamfer array', async ({
|
||||
app,
|
||||
editor,
|
||||
toolbar,
|
||||
scene,
|
||||
}) => {
|
||||
test(
|
||||
'works on all edge selections and can break up multi edges in a chamfer array',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ app, editor, toolbar, scene }) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'Fails on windows in CI, can not be replicated locally on windows.'
|
||||
@ -360,14 +356,13 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test('Works on chamfers that are non in a pipeExpression can break up multi edges in a chamfer array', async ({
|
||||
app,
|
||||
editor,
|
||||
toolbar,
|
||||
scene,
|
||||
}) => {
|
||||
test(
|
||||
'Works on chamfers that are non in a pipeExpression can break up multi edges in a chamfer array',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ app, editor, toolbar, scene }) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'Fails on windows in CI, can not be replicated locally on windows.'
|
||||
@ -449,5 +444,6 @@ const sketch002 = startSketchOn(extrude001, seg03)
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -17,6 +17,43 @@ test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
})
|
||||
|
||||
test(
|
||||
'projects reload if a new one is created, deleted, or renamed externally',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const externalCreatedProjectName = 'external-created-project'
|
||||
|
||||
let targetDir = ''
|
||||
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
targetDir = dir
|
||||
setTimeout(async () => {
|
||||
const myDir = join(dir, externalCreatedProjectName)
|
||||
await fsp.mkdir(myDir)
|
||||
}, 1000)
|
||||
},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
const projectLinks = page.getByTestId('project-link')
|
||||
|
||||
await projectLinks.first().waitFor()
|
||||
await expect(projectLinks).toHaveText(externalCreatedProjectName)
|
||||
|
||||
await fsp.rename(join(targetDir, externalCreatedProjectName), join(targetDir, externalCreatedProjectName + '1'))
|
||||
await expect(projectLinks).toHaveText(externalCreatedProjectName + '1')
|
||||
|
||||
await fsp.rm(join(targetDir, externalCreatedProjectName), { recursive: true, force: true })
|
||||
const projectsTotal = await projectLinks.count()
|
||||
await expect(projectsTotal).toBe(0)
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'click help/keybindings from home page',
|
||||
{ tag: '@electron' },
|
||||
|
@ -221,7 +221,10 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
// Make sure it's not a link
|
||||
await expect(zooLogo).not.toHaveAttribute('href')
|
||||
})
|
||||
test('Position _ Is Out Of Range... regression test', async ({ page }) => {
|
||||
test(
|
||||
'Position _ Is Out Of Range... regression test',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ page }) => {
|
||||
// SKip on windows, its being weird.
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
@ -298,7 +301,8 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
thing: "blah"`)
|
||||
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test('when engine fails export we handle the failure and alert the user', async ({
|
||||
page,
|
||||
@ -401,9 +405,10 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
const successToastMessage = page.getByText(`Exported successfully`)
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
})
|
||||
test('ensure you can not export while an export is already going', async ({
|
||||
page,
|
||||
}) => {
|
||||
test(
|
||||
'ensure you can not export while an export is already going',
|
||||
{ tag: ['@skipLinux', '@skipWin'] },
|
||||
async ({ page }) => {
|
||||
// This is being weird on ubuntu and windows.
|
||||
test.skip(
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
@ -489,7 +494,8 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`Network health indicator only appears in modeling view`,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { test, expect, Page } from '@playwright/test'
|
||||
import { test as test2, expect as expect2 } from './fixtures/fixtureSetup'
|
||||
|
||||
import {
|
||||
getMovementUtils,
|
||||
@ -1107,3 +1108,64 @@ const sketch002 = startSketchOn(extrude001, 'END')
|
||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||
})
|
||||
})
|
||||
|
||||
test2.describe('Sketch mode should be toleratant to syntax errors', () => {
|
||||
test2(
|
||||
'adding a syntax error, recovers after fixing',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ app, scene, editor, toolbar }) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'a codemirror error appears in this test only on windows, that causes the test to fail only because of our "no new error" logic, but it can not be replicated locally'
|
||||
)
|
||||
const file = await app.getInputFile('e2e-can-sketch-on-chamfer.kcl')
|
||||
await app.initialise(file)
|
||||
|
||||
const [objClick] = scene.makeMouseHelpers(600, 250)
|
||||
const arrowHeadLocation = { x: 604, y: 129 } as const
|
||||
const arrowHeadWhite: [number, number, number] = [255, 255, 255]
|
||||
const backgroundGray: [number, number, number] = [28, 28, 28]
|
||||
const verifyArrowHeadColor = async (c: [number, number, number]) =>
|
||||
scene.expectPixelColor(c, arrowHeadLocation, 15)
|
||||
|
||||
await test.step('check chamfer selection changes cursor positon', async () => {
|
||||
await expect2(async () => {
|
||||
// sometimes initial click doesn't register
|
||||
await objClick()
|
||||
await editor.expectActiveLinesToBe([
|
||||
'|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]',
|
||||
])
|
||||
}).toPass({ timeout: 15_000, intervals: [500] })
|
||||
})
|
||||
|
||||
await test.step('enter sketch and sanity check segments have been drawn', async () => {
|
||||
await toolbar.editSketch()
|
||||
// this checks sketch segments have been drawn
|
||||
await verifyArrowHeadColor(arrowHeadWhite)
|
||||
})
|
||||
|
||||
await test.step('Make typo and check the segments have Disappeared and there is a syntax error', async () => {
|
||||
await editor.replaceCode('lineTo([pro', 'badBadBadFn([pro')
|
||||
await editor.expectState({
|
||||
activeLines: [],
|
||||
diagnostics: ['memoryitemkey`badBadBadFn`isnotdefined'],
|
||||
highlightedCode: '',
|
||||
})
|
||||
// this checks sketch segments have failed to be drawn
|
||||
await verifyArrowHeadColor(backgroundGray)
|
||||
})
|
||||
|
||||
await test.step('', async () => {
|
||||
await editor.replaceCode('badBadBadFn([pro', 'lineTo([pro')
|
||||
await editor.expectState({
|
||||
activeLines: [],
|
||||
diagnostics: [],
|
||||
highlightedCode: '',
|
||||
})
|
||||
// this checks sketch segments have been drawn
|
||||
await verifyArrowHeadColor(arrowHeadWhite)
|
||||
})
|
||||
await app.page.waitForTimeout(100)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -49,7 +49,7 @@ test.setTimeout(60_000)
|
||||
|
||||
test(
|
||||
'exports of each format should work',
|
||||
{ tag: '@snapshot' },
|
||||
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] },
|
||||
async ({ page, context }) => {
|
||||
// skip on macos and windows.
|
||||
test.skip(
|
||||
|
@ -438,34 +438,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
}
|
||||
return maxDiff
|
||||
},
|
||||
getPixelRGBs: async (
|
||||
coords: { x: number; y: number },
|
||||
radius: number
|
||||
): Promise<[number, number, number][]> => {
|
||||
const buffer = await page.screenshot({
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot = await PNG.sync.read(buffer)
|
||||
const pixMultiplier: number = await page.evaluate(
|
||||
'window.devicePixelRatio'
|
||||
)
|
||||
const allCords: [number, number][] = [[coords.x, coords.y]]
|
||||
for (let i = 1; i < radius; i++) {
|
||||
allCords.push([coords.x + i, coords.y])
|
||||
allCords.push([coords.x - i, coords.y])
|
||||
allCords.push([coords.x, coords.y + i])
|
||||
allCords.push([coords.x, coords.y - i])
|
||||
}
|
||||
return allCords.map(([x, y]) => {
|
||||
const index =
|
||||
(screenshot.width * y * pixMultiplier + x * pixMultiplier) * 4 // rbga is 4 channels
|
||||
return [
|
||||
screenshot.data[index],
|
||||
screenshot.data[index + 1],
|
||||
screenshot.data[index + 2],
|
||||
]
|
||||
})
|
||||
},
|
||||
getPixelRGBs: getPixelRGBs(page),
|
||||
doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) =>
|
||||
doAndWaitForImageDiff(page, fn, diffCount),
|
||||
emulateNetworkConditions: async (
|
||||
@ -1070,3 +1043,32 @@ export async function openAndClearDebugPanel(page: Page) {
|
||||
export function sansWhitespace(str: string) {
|
||||
return str.replace(/\s+/g, '').trim()
|
||||
}
|
||||
|
||||
export function getPixelRGBs(page: Page) {
|
||||
return async (
|
||||
coords: { x: number; y: number },
|
||||
radius: number
|
||||
): Promise<[number, number, number][]> => {
|
||||
const buffer = await page.screenshot({
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot = await PNG.sync.read(buffer)
|
||||
const pixMultiplier: number = await page.evaluate('window.devicePixelRatio')
|
||||
const allCords: [number, number][] = [[coords.x, coords.y]]
|
||||
for (let i = 1; i < radius; i++) {
|
||||
allCords.push([coords.x + i, coords.y])
|
||||
allCords.push([coords.x - i, coords.y])
|
||||
allCords.push([coords.x, coords.y + i])
|
||||
allCords.push([coords.x, coords.y - i])
|
||||
}
|
||||
return allCords.map(([x, y]) => {
|
||||
const index =
|
||||
(screenshot.width * y * pixMultiplier + x * pixMultiplier) * 4 // rbga is 4 channels
|
||||
return [
|
||||
screenshot.data[index],
|
||||
screenshot.data[index + 1],
|
||||
screenshot.data[index + 2],
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,10 @@ test.afterEach(async ({ page }, testInfo) => {
|
||||
|
||||
test.describe('Testing selections', () => {
|
||||
test.setTimeout(90_000)
|
||||
test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
test(
|
||||
'Selections work on fresh and edited sketch',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ page }) => {
|
||||
// Skip on windows its being weird.
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
|
||||
@ -181,7 +184,9 @@ test.describe('Testing selections', () => {
|
||||
|
||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.up(process.platform === 'linux' ? 'Control' : 'Meta')
|
||||
await page.keyboard.up(
|
||||
process.platform === 'linux' ? 'Control' : 'Meta'
|
||||
)
|
||||
|
||||
// clear selection by clicking on nothing
|
||||
await emptySpaceClick()
|
||||
@ -242,7 +247,8 @@ test.describe('Testing selections', () => {
|
||||
await test.step(`Test hovering and selecting on edited sketch`, async () => {
|
||||
await selectionSequence()
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test('Solids should be select and deletable', async ({ page }) => {
|
||||
test.setTimeout(90_000)
|
||||
|
@ -258,7 +258,7 @@ test.describe('Testing settings', () => {
|
||||
|
||||
test(
|
||||
`Project settings override user settings on desktop`,
|
||||
{ tag: '@electron' },
|
||||
{ tag: ['@electron', '@skipWin'] },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
|
@ -447,9 +447,10 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await expect(page.getByText(promptWithNewline)).toBeVisible()
|
||||
})
|
||||
|
||||
test('can do many at once and get many prompts back, and interact with many', async ({
|
||||
page,
|
||||
}) => {
|
||||
test(
|
||||
'can do many at once and get many prompts back, and interact with many',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ page }) => {
|
||||
// Let this test run longer since we've seen it timeout.
|
||||
test.setTimeout(180_000)
|
||||
// skip on windows
|
||||
@ -565,7 +566,8 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await expect(closeButton).toBeVisible()
|
||||
await closeButton.click()
|
||||
await expect(successToastMessage).not.toBeVisible()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test('can do many at once with errors, clicking dismiss error does not dismiss all', async ({
|
||||
page,
|
||||
|
4
interface.d.ts
vendored
4
interface.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import fsSync from 'node:fs'
|
||||
import path from 'path'
|
||||
import { dialog, shell } from 'electron'
|
||||
import { MachinesListing } from 'lib/machineManager'
|
||||
@ -17,6 +18,9 @@ export interface IElectronAPI {
|
||||
platform: typeof process.env.platform
|
||||
arch: typeof process.env.arch
|
||||
version: typeof process.env.version
|
||||
watchFileOn: (path: string, callback: (eventType: string, path: string) => void)=> void
|
||||
watchFileOff: (path: string) => void
|
||||
watchFileObliterate: () => void
|
||||
readFile: (path: string) => ReturnType<fs.readFile>
|
||||
writeFile: (
|
||||
path: string,
|
||||
|
25
package.json
25
package.json
@ -75,11 +75,10 @@
|
||||
"build:both": "vite build",
|
||||
"build:both:local": "yarn build:wasm && vite build",
|
||||
"pretest": "yarn remove-importmeta",
|
||||
"test": "vitest --mode development",
|
||||
"test:nowatch": "vitest run --mode development",
|
||||
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests --benches)",
|
||||
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||
"simpleserver:bg": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"fetch:wasm": "./get-latest-wasm-bundle.sh",
|
||||
@ -89,7 +88,8 @@
|
||||
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
|
||||
"lint": "eslint --fix src e2e packages/codemirror-lsp-client",
|
||||
"lint-fix": "eslint --fix src e2e packages/codemirror-lsp-client",
|
||||
"lint": "eslint --max-warnings 0 src e2e packages/codemirror-lsp-client",
|
||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
|
||||
"postinstall": "yarn fetch:samples && yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
|
||||
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
|
||||
@ -101,7 +101,23 @@
|
||||
"tron:publish": "electron-forge publish",
|
||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
|
||||
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
||||
"tronb:package": "electron-builder --config electron-builder.yml"
|
||||
"tronb:package": "electron-builder --config electron-builder.yml",
|
||||
"test-setup": "yarn install && yarn build:wasm",
|
||||
"test": "vitest --mode development",
|
||||
"test:nowatch": "vitest run --mode development",
|
||||
"test:unit": "vitest run --mode development",
|
||||
"test:playwright:browser:chrome": "playwright test '--project=Google Chrome' --config=playwright.ci.config.ts '--grep-invert=@snapshot|@electron'",
|
||||
"test:playwright:browser:chrome:windows": "playwright test '--project=Google Chrome' --config=playwright.ci.config.ts '--grep-invert=@snapshot|@electron|@skipWin'",
|
||||
"test:playwright:browser:chrome:ubuntu": "playwright test '--project=Google Chrome' --config=playwright.ci.config.ts '--grep-invert=@snapshot|@electron|@skipLinux'",
|
||||
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep=@electron",
|
||||
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipWin",
|
||||
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipMacos",
|
||||
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipLinux",
|
||||
"test:playwright:electron:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron",
|
||||
"test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipWin",
|
||||
"test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipMacos",
|
||||
"test:playwright:electron:ubuntu:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipLinux",
|
||||
"test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
@ -174,6 +190,7 @@
|
||||
"happy-dom": "^14.3.10",
|
||||
"http-server": "^14.1.1",
|
||||
"husky": "^9.1.5",
|
||||
"kill-port": "^2.0.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
|
@ -1175,7 +1175,7 @@ export class SceneEntities {
|
||||
prepareTruncatedMemoryAndAst(
|
||||
sketchPathToNode,
|
||||
ast || kclManager.ast,
|
||||
kclManager.programMemory,
|
||||
kclManager.lastSuccessfulProgramMemory,
|
||||
draftSegment
|
||||
)
|
||||
onDragSegment({
|
||||
|
47
src/hooks/useFileSystemWatcher.tsx
Normal file
47
src/hooks/useFileSystemWatcher.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import fs from 'node:fs'
|
||||
|
||||
type Path = string
|
||||
|
||||
type WatcherCallback = (eventType: string, path: string) => void
|
||||
|
||||
// Not having access to NodeJS functions has influenced the design a little.
|
||||
// There is some indirection going on because we can only pass data between
|
||||
// the NodeJS<->Browser boundary. The actual functions need to run on the
|
||||
// NodeJS side. Because EventEmitters come bundled with their listener
|
||||
// methods it complicates things because we can't just do
|
||||
// watcher.addListener(() => { ... }).
|
||||
|
||||
export const useFileSystemWatcher = (callback: (path: Path) => void, dependencyArray: Path[]): void => {
|
||||
// Used to track if dependencyArrray changes.
|
||||
const [dependencyArrayTracked, setDependencyArrayTracked] = useState<Path[]>([])
|
||||
|
||||
// On component teardown obliterate all watchers.
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
window.electron.watchFileObliterate()
|
||||
}
|
||||
}, [])
|
||||
|
||||
function difference<T>(l1: T[], l2: T[]): [T[], T[]] {
|
||||
return [
|
||||
l1.filter((x) => Boolean(!l2.find((x2) => x2 === x))),
|
||||
l1.filter((x) => Boolean(l2.find((x2) => x2 === x)))
|
||||
]
|
||||
}
|
||||
|
||||
// Removing 1 watcher at a time is only possible because in a filesystem,
|
||||
// a path is unique (there can never be two paths with the same name).
|
||||
// Otherwise we would have to obliterate() the whole list and reconstruct it.
|
||||
useEffect(() => {
|
||||
const [pathsRemoved, pathsRemaining] = difference(dependencyArrayTracked, dependencyArray)
|
||||
for (let path of pathsRemoved) {
|
||||
window.electron.watchFileOff(path)
|
||||
}
|
||||
const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked)
|
||||
for (let path of pathsAdded) {
|
||||
window.electron.watchFileOn(path, (_eventType: string, path: Path) => callback(path))
|
||||
}
|
||||
setDependencyArrayTracked(pathsRemaining.concat(pathsAdded))
|
||||
}, dependencyArray)
|
||||
}
|
@ -43,6 +43,7 @@ export class KclManager {
|
||||
digest: null,
|
||||
}
|
||||
private _programMemory: ProgramMemory = ProgramMemory.empty()
|
||||
lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty()
|
||||
private _logs: string[] = []
|
||||
private _lints: Diagnostic[] = []
|
||||
private _kclErrors: KCLError[] = []
|
||||
@ -297,6 +298,9 @@ export class KclManager {
|
||||
// Do not add the errors since the program was interrupted and the error is not a real KCL error
|
||||
this.addKclErrors(isInterrupted ? [] : errors)
|
||||
this.programMemory = programMemory
|
||||
if (!errors.length) {
|
||||
this.lastSuccessfulProgramMemory = programMemory
|
||||
}
|
||||
this.ast = { ...ast }
|
||||
this._executeCallback()
|
||||
this.engineCommandManager.addCommandLog({
|
||||
@ -342,6 +346,9 @@ export class KclManager {
|
||||
this._logs = logs
|
||||
this._kclErrors = errors
|
||||
this._programMemory = programMemory
|
||||
if (!errors.length) {
|
||||
this.lastSuccessfulProgramMemory = programMemory
|
||||
}
|
||||
if (updates !== 'artifactRanges') return
|
||||
|
||||
// TODO the below seems like a work around, I wish there's a comment explaining exactly what
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
} from './artifactGraph'
|
||||
import { err } from 'lib/trap'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { CI, VITE_KC_DEV_TOKEN } from 'env'
|
||||
import { VITE_KC_DEV_TOKEN } from 'env'
|
||||
import fsp from 'fs/promises'
|
||||
import fs from 'fs'
|
||||
import { chromium } from 'playwright'
|
||||
@ -97,21 +97,6 @@ type CacheShape = {
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
|
||||
let parsed
|
||||
try {
|
||||
const file = await fsp.readFile(fullPath, 'utf-8')
|
||||
parsed = JSON.parse(file)
|
||||
} catch (e) {
|
||||
parsed = false
|
||||
}
|
||||
|
||||
if (!CI && parsed) {
|
||||
// caching the results of the websocket commands makes testing this locally much faster
|
||||
// real calls to the engine are needed to test the artifact map
|
||||
// bust the cache with: `rm -rf src/lang/std/artifactGraphCache`
|
||||
return
|
||||
}
|
||||
|
||||
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
|
||||
await new Promise((resolve) => {
|
||||
engineCommandManager.start({
|
||||
|
@ -99,6 +99,6 @@ export const MAKE_TOAST_MESSAGES = {
|
||||
/** The URL for the KCL samples manifest files */
|
||||
export const KCL_SAMPLES_MANIFEST_URLS = {
|
||||
remote:
|
||||
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifst.json',
|
||||
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifest.json',
|
||||
localFallback: '/kcl-samples-manifest-fallback.json',
|
||||
} as const
|
||||
|
@ -185,7 +185,6 @@ export const homeLoader: LoaderFunction = async (): Promise<
|
||||
return redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||
}
|
||||
const { configuration } = await loadAndValidateSettings()
|
||||
|
||||
const projectDir = await ensureProjectDirectoryExists(configuration)
|
||||
|
||||
if (projectDir) {
|
||||
@ -193,10 +192,12 @@ export const homeLoader: LoaderFunction = async (): Promise<
|
||||
|
||||
return {
|
||||
projects,
|
||||
projectDir,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
projects: [],
|
||||
projectDir: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ export type FileLoaderData = {
|
||||
|
||||
export type HomeLoaderData = {
|
||||
projects: Project[]
|
||||
projectDir?: string
|
||||
}
|
||||
|
||||
// From the very helpful @jcalz on StackOverflow: https://stackoverflow.com/a/58436959/22753272
|
||||
|
@ -23,6 +23,27 @@ const isMac = os.platform() === 'darwin'
|
||||
const isWindows = os.platform() === 'win32'
|
||||
const isLinux = os.platform() === 'linux'
|
||||
|
||||
let fsWatchListeners = new Map<string, { watcher: fsSync.FSWatcher, callback: (eventType: string, path: string) => void }>()
|
||||
|
||||
const watchFileOn = (path: string, callback: (eventType: string, path: string) => void) => {
|
||||
const watcher = fsSync.watch(path)
|
||||
watcher.on('change', callback)
|
||||
fsWatchListeners.set(path, { watcher, callback })
|
||||
}
|
||||
const watchFileOff = (path: string) => {
|
||||
const entry = fsWatchListeners.get(path)
|
||||
if (!entry) return
|
||||
const { watcher, callback } = entry
|
||||
watcher.off('change', callback)
|
||||
watcher.close()
|
||||
fsWatchListeners.delete(path)
|
||||
}
|
||||
const watchFileObliterate = () => {
|
||||
for (let [pathAsKey] of fsWatchListeners) {
|
||||
watchFileOff(pathAsKey)
|
||||
}
|
||||
fsWatchListeners = new Map()
|
||||
}
|
||||
const readFile = (path: string) => fs.readFile(path, 'utf-8')
|
||||
// It seems like from the node source code this does not actually block but also
|
||||
// don't trust me on that (jess).
|
||||
@ -71,6 +92,9 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
// Passing fs directly is not recommended since it gives a lot of power
|
||||
// to the browser side / potential malicious code. We restrict what is
|
||||
// exported.
|
||||
watchFileOn,
|
||||
watchFileOff,
|
||||
watchFileObliterate,
|
||||
readFile,
|
||||
writeFile,
|
||||
exists,
|
||||
|
@ -38,11 +38,12 @@ import {
|
||||
} from 'lib/desktop'
|
||||
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
|
||||
import { Project } from 'lib/project'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
|
||||
// This route only opens in the desktop context for now,
|
||||
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
||||
const Home = () => {
|
||||
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
|
||||
const { projects: loadedProjects, projectDir } = useLoaderData() as HomeLoaderData
|
||||
useRefreshSettings(PATHS.HOME + 'SETTINGS')
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const navigate = useNavigate()
|
||||
@ -51,6 +52,9 @@ const Home = () => {
|
||||
} = useSettingsAuthContext()
|
||||
const { onProjectOpen } = useLspContext()
|
||||
|
||||
// Reload home / projects listing if the projectDir has any updates.
|
||||
useFileSystemWatcher(() => { navigate(0) }, projectDir ? [projectDir] : [])
|
||||
|
||||
// Cancel all KCL executions while on the home page
|
||||
useEffect(() => {
|
||||
kclManager.cancelAllExecutions()
|
||||
|
18
yarn.lock
18
yarn.lock
@ -5695,6 +5695,11 @@ get-symbol-description@^1.0.2:
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.4"
|
||||
|
||||
get-them-args@1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/get-them-args/-/get-them-args-1.3.2.tgz#74a20ba8a4abece5ae199ad03f2bcc68fdfc9ba5"
|
||||
integrity sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw==
|
||||
|
||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
@ -6645,6 +6650,14 @@ keyv@^4.0.0, keyv@^4.5.3:
|
||||
dependencies:
|
||||
json-buffer "3.0.1"
|
||||
|
||||
kill-port@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/kill-port/-/kill-port-2.0.1.tgz#e5e18e2706b13d54320938be42cb7d40609b15cf"
|
||||
integrity sha512-e0SVOV5jFo0mx8r7bS29maVWp17qGqLBZ5ricNSajON6//kmb7qqqNnml4twNE8Dtj97UQD+gNFOaipS/q1zzQ==
|
||||
dependencies:
|
||||
get-them-args "1.3.2"
|
||||
shell-exec "1.0.2"
|
||||
|
||||
klaw@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/klaw/-/klaw-4.1.0.tgz#5df608067d8cb62bbfb24374f8e5d956323338f3"
|
||||
@ -8579,6 +8592,11 @@ shebang-regex@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shell-exec@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/shell-exec/-/shell-exec-1.0.2.tgz#2e9361b0fde1d73f476c4b6671fa17785f696756"
|
||||
integrity sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==
|
||||
|
||||
side-channel@^1.0.4, side-channel@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
|
||||
|
Reference in New Issue
Block a user