Compare commits
45 Commits
jtran/recu
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
682590deea | |||
925f5cc2c2 | |||
a167c174f9 | |||
7f297c13fd | |||
a7e3d83297 | |||
f74c12aa99 | |||
5df9965795 | |||
50d80eb0b6 | |||
96d24065d6 | |||
61dc94b1ee | |||
f14c27e1c4 | |||
c09775f5eb | |||
d14b8f5443 | |||
4a14ca38ab | |||
3543c5f0e7 | |||
a0dc5f4a89 | |||
9d148938a2 | |||
9c6cca2944 | |||
5c472c63d2 | |||
f77b312ecb | |||
8a66d0df76 | |||
b3dc3ff78c | |||
d02df08471 | |||
aac758b396 | |||
0ef6eac239 | |||
c674feb782 | |||
fba3d7c5c1 | |||
8b8fb696d0 | |||
d05f3c00b9 | |||
2541e0c0ea | |||
5e5a204244 | |||
032c2fdd24 | |||
27883e7800 | |||
1ccb810e23 | |||
1c83f148d9 | |||
c7f533b38e | |||
2b711d216f | |||
c67511f67c | |||
d9423219d1 | |||
3f270d8bcf | |||
4c7b72329d | |||
4c060f3d2f | |||
f3afbe8a7b | |||
dad7a84798 | |||
1a560fdc6a |
@ -25,7 +25,9 @@
|
||||
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
||||
"rules": {
|
||||
"@typescript-eslint/no-floating-promises": "warn",
|
||||
"testing-library/prefer-screen-queries": "off"
|
||||
"suggest-no-throw/suggest-no-throw": "off",
|
||||
"testing-library/prefer-screen-queries": "off",
|
||||
"jest/valid-expect": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -345,7 +345,7 @@ jobs:
|
||||
cat last_download.json
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v2.1.3'
|
||||
uses: 'google-github-actions/auth@v2.1.5'
|
||||
with:
|
||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||
|
||||
@ -355,7 +355,7 @@ jobs:
|
||||
project_id: kittycadapi
|
||||
|
||||
- name: Upload release files to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.0
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
||||
with:
|
||||
path: artifact
|
||||
glob: '*/Zoo*'
|
||||
@ -363,13 +363,13 @@ jobs:
|
||||
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
||||
|
||||
- name: Upload update endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.0
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
||||
with:
|
||||
path: last_update.json
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
||||
- name: Upload download endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.0
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.3
|
||||
with:
|
||||
path: last_download.json
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
2
.github/workflows/build-test-web.yml
vendored
@ -44,6 +44,8 @@ jobs:
|
||||
- run: yarn build:wasm
|
||||
- run: yarn xstate:typegen
|
||||
- run: yarn tsc
|
||||
- name: Lint
|
||||
run: yarn eslint --max-warnings 0 src e2e
|
||||
|
||||
|
||||
check-typos:
|
||||
|
2
.github/workflows/cargo-test.yml
vendored
@ -7,6 +7,7 @@ on:
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- 'src/wasm-lib/**.kcl'
|
||||
- .github/workflows/cargo-test.yml
|
||||
|
||||
pull_request:
|
||||
@ -15,6 +16,7 @@ on:
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- 'src/wasm-lib/**.kcl'
|
||||
- .github/workflows/cargo-test.yml
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
|
31
.github/workflows/label-issues.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
name: Label Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
permissions:
|
||||
issues: write
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check if issue opener is ZooSpiritWolf
|
||||
id: check_opener
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issueOpener = context.payload.issue.user.login;
|
||||
return issueOpener === 'ZooSpiritWolf';
|
||||
|
||||
- name: Add labels
|
||||
if: steps.check_opener.outputs.result == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
github.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
labels: ['bug', 'regression', 'high-priority']
|
||||
});
|
2
.github/workflows/playwright.yml
vendored
@ -346,7 +346,7 @@ jobs:
|
||||
run: yarn build:wasm
|
||||
- name: build electron
|
||||
shell: bash
|
||||
run: yarn electron:package
|
||||
run: yarn tron:package
|
||||
- uses: actions/download-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
continue-on-error: true
|
||||
|
@ -101,7 +101,7 @@ This will start the application and hot-reload on changed.
|
||||
|
||||
Devtools can be opened with the usual Cmd/Ctrl-Shift-I.
|
||||
|
||||
To build, run `yarn electron:package`.
|
||||
To build, run `yarn tron:package`.
|
||||
|
||||
## Checking out commits / Bisecting
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
import { getUtils, setup, tearDown } from './test-utils'
|
||||
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates'
|
||||
import fsp from 'fs/promises'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
@ -83,7 +84,7 @@ test.describe('Code pane and errors', () => {
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(page.getByText('Unexpected token').first()).toBeVisible()
|
||||
await expect(page.getByText('Unexpected token: |').first()).toBeVisible()
|
||||
|
||||
// Close the code pane
|
||||
await codePaneButton.click()
|
||||
@ -106,7 +107,7 @@ test.describe('Code pane and errors', () => {
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(page.getByText('Unexpected token').first()).toBeVisible()
|
||||
await expect(page.getByText('Unexpected token: |').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('When error is not in view you can click the badge to scroll to it', async ({
|
||||
@ -217,3 +218,93 @@ test.describe('Code pane and errors', () => {
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test(
|
||||
'Opening multiple panes persists when switching projects',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
// Setup multiple projects.
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await Promise.all([
|
||||
fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }),
|
||||
fsp.mkdir(`${dir}/bracket`, { recursive: true }),
|
||||
])
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
|
||||
`${dir}/router-template-slate/main.kcl`
|
||||
),
|
||||
fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await test.step('Opening the bracket project should load', async () => {
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
})
|
||||
|
||||
// If they're open by default, we're not actually testing anything.
|
||||
await test.step('Pre-condition: panes are not already visible', async () => {
|
||||
await expect(page.locator('#variables-pane')).not.toBeVisible()
|
||||
await expect(page.locator('#logs-pane')).not.toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Open multiple panes', async () => {
|
||||
await u.openKclCodePanel()
|
||||
await u.openVariablesPane()
|
||||
await u.openLogsPane()
|
||||
})
|
||||
|
||||
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
|
||||
await page.getByTestId('app-logo').click()
|
||||
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Opening the router-template project should load', async () => {
|
||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||
|
||||
await page.getByText('router-template-slate').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('All panes opened before should be visible', async () => {
|
||||
await expect(page.locator('#code-pane')).toBeVisible()
|
||||
await expect(page.locator('#variables-pane')).toBeVisible()
|
||||
await expect(page.locator('#logs-pane')).toBeVisible()
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
})
|
||||
test.describe('Copilot ghost text', () => {
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
test.skip(true, 'Needs to get covered again')
|
||||
|
||||
test('completes code in empty file', async ({ page }) => {
|
||||
|
188
e2e/playwright/desktop-export.spec.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { getUtils, setupElectron, tearDown } from './test-utils'
|
||||
import fsp from 'fs/promises'
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
})
|
||||
|
||||
test(
|
||||
'export works on the first try',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await Promise.all([fsp.mkdir(`${dir}/bracket`, { recursive: true })])
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
|
||||
`${dir}/bracket/other.kcl`
|
||||
),
|
||||
fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
),
|
||||
])
|
||||
},
|
||||
})
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
await test.step('on open of project', async () => {
|
||||
await expect(page.getByText(`bracket`)).toBeVisible()
|
||||
|
||||
// open the project
|
||||
await page.getByText(`bracket`).click()
|
||||
|
||||
// wait for the project to load
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// expect zero errors in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
// export the model
|
||||
const exportButton = page.getByTestId('export-pane-button')
|
||||
await expect(exportButton).toBeVisible()
|
||||
|
||||
const gltfOption = page.getByText('glTF')
|
||||
const submitButton = page.getByText('Confirm Export')
|
||||
const exportingToastMessage = page.getByText(`Exporting...`)
|
||||
const errorToastMessage = page.getByText(`Error while exporting`)
|
||||
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
||||
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
|
||||
|
||||
// Click the export button
|
||||
await exportButton.click()
|
||||
|
||||
await expect(gltfOption).toBeVisible()
|
||||
await expect(page.getByText('STL')).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Click the checkbox
|
||||
await expect(submitButton).toBeVisible()
|
||||
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
||||
|
||||
// Expect it to succeed.
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
|
||||
const successToastMessage = page.getByText(`Exported successfully`)
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
|
||||
await test.step('Check the export size', async () => {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
try {
|
||||
const outputGltf = await fsp.readFile('output.gltf')
|
||||
return outputGltf.byteLength
|
||||
} catch (e) {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBe(477327)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('on open of file in file pane', async () => {
|
||||
const u = await getUtils(page)
|
||||
await u.openFilePanel()
|
||||
|
||||
const otherKclButton = page.getByRole('button', { name: 'other.kcl' })
|
||||
|
||||
// Click the file
|
||||
await otherKclButton.click()
|
||||
|
||||
// Close the file pane
|
||||
await u.closeFilePanel()
|
||||
|
||||
// wait for it to finish executing (todo: make this more robust)
|
||||
await page.waitForTimeout(1000)
|
||||
// expect zero errors in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
// export the model
|
||||
const exportButton = page.getByTestId('export-pane-button')
|
||||
await expect(exportButton).toBeVisible()
|
||||
|
||||
const gltfOption = page.getByText('glTF')
|
||||
const submitButton = page.getByText('Confirm Export')
|
||||
const exportingToastMessage = page.getByText(`Exporting...`)
|
||||
const errorToastMessage = page.getByText(`Error while exporting`)
|
||||
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
||||
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
|
||||
|
||||
// Click the export button
|
||||
await exportButton.click()
|
||||
|
||||
await expect(gltfOption).toBeVisible()
|
||||
await expect(page.getByText('STL')).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Click the checkbox
|
||||
await expect(submitButton).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
||||
|
||||
// Expect it to succeed.
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
|
||||
const successToastMessage = page.getByText(`Exported successfully`)
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
|
||||
await test.step('Check the export size', async () => {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
try {
|
||||
const outputGltf = await fsp.readFile('output.gltf')
|
||||
return outputGltf.byteLength
|
||||
} catch (e) {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBe(105022)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
})
|
||||
await electronApp.close()
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
@ -354,7 +354,7 @@ test.describe('Editor tests', () => {
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(page.getByText('Unexpected token').first()).toBeVisible()
|
||||
await expect(page.getByText('Unexpected token: $').first()).toBeVisible()
|
||||
|
||||
// select the line that's causing the error and delete it
|
||||
await page.getByText('$ error').click()
|
||||
@ -714,17 +714,15 @@ test.describe('Editor tests', () => {
|
||||
|> close(%)`)
|
||||
})
|
||||
|
||||
// failing for the same reason as "Can edit a sketch that has been extruded in the same pipe"
|
||||
// please fix together
|
||||
test.fixme('Can undo a sketch modification with ctrl+z', async ({ page }) => {
|
||||
test('Can undo a sketch modification with ctrl+z', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([4.61, -14.01], %)
|
||||
|> startProfileAt([4.61, -10.01], %)
|
||||
|> line([12.73, -0.09], %)
|
||||
|> tangentialArcTo([24.95, -5.38], %)
|
||||
|> tangentialArcTo([24.95, -0.38], %)
|
||||
|> close(%)
|
||||
|> extrude(5, %)`
|
||||
)
|
||||
@ -759,11 +757,11 @@ test.describe('Editor tests', () => {
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const startPX = [665, 458]
|
||||
const startPX = [665, 397]
|
||||
|
||||
const dragPX = 40
|
||||
|
||||
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
|
||||
await page.getByText('startProfileAt([4.61, -10.01], %)').click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeVisible()
|
||||
@ -801,7 +799,7 @@ test.describe('Editor tests', () => {
|
||||
// drag tangentialArcTo handle
|
||||
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
|
||||
await page.dragAndDrop('#stream', '#stream', {
|
||||
sourcePosition: { x: tangentEnd.x, y: tangentEnd.y - 5 },
|
||||
sourcePosition: { x: tangentEnd.x + 10, y: tangentEnd.y - 5 },
|
||||
targetPosition: {
|
||||
x: tangentEnd.x + dragPX,
|
||||
y: tangentEnd.y + dragPX,
|
||||
@ -813,12 +811,12 @@ test.describe('Editor tests', () => {
|
||||
// expect the code to have changed
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([7.12, -16.82], %)
|
||||
|> line([15.4, -2.74], %)
|
||||
|> tangentialArcTo([24.95, -5.38], %)
|
||||
|> line([2.65, -2.69], %)
|
||||
|> close(%)
|
||||
|> extrude(5, %)`)
|
||||
|> startProfileAt([7.12, -12.68], %)
|
||||
|> line([15.39, -2.78], %)
|
||||
|> tangentialArcTo([27.6, -3.05], %)
|
||||
|> close(%)
|
||||
|> extrude(5, %)
|
||||
`)
|
||||
|
||||
// Hit undo
|
||||
await page.keyboard.down('Control')
|
||||
@ -827,11 +825,11 @@ test.describe('Editor tests', () => {
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([7.12, -16.82], %)
|
||||
|> line([15.4, -2.74], %)
|
||||
|> tangentialArcTo([24.95, -5.38], %)
|
||||
|> close(%)
|
||||
|> extrude(5, %)`)
|
||||
|> startProfileAt([7.12, -12.68], %)
|
||||
|> line([15.39, -2.78], %)
|
||||
|> tangentialArcTo([24.95, -0.38], %)
|
||||
|> close(%)
|
||||
|> extrude(5, %)`)
|
||||
|
||||
// Hit undo again.
|
||||
await page.keyboard.down('Control')
|
||||
@ -840,11 +838,12 @@ test.describe('Editor tests', () => {
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([7.12, -16.82], %)
|
||||
|> line([12.73, -0.09], %)
|
||||
|> tangentialArcTo([24.95, -5.38], %)
|
||||
|> close(%)
|
||||
|> extrude(5, %)`)
|
||||
|> startProfileAt([7.12, -12.68], %)
|
||||
|> line([12.73, -0.09], %)
|
||||
|> tangentialArcTo([24.95, -0.38], %)
|
||||
|> close(%)
|
||||
|> extrude(5, %)
|
||||
`)
|
||||
|
||||
// Hit undo again.
|
||||
await page.keyboard.down('Control')
|
||||
@ -854,9 +853,9 @@ test.describe('Editor tests', () => {
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([4.61, -14.01], %)
|
||||
|> startProfileAt([4.61, -10.01], %)
|
||||
|> line([12.73, -0.09], %)
|
||||
|> tangentialArcTo([24.95, -5.38], %)
|
||||
|> tangentialArcTo([24.95, -0.38], %)
|
||||
|> close(%)
|
||||
|> extrude(5, %)`)
|
||||
})
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 71 KiB |
102
e2e/playwright/machines.spec.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { setupElectron, tearDown } from './test-utils'
|
||||
import fsp from 'fs/promises'
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
})
|
||||
|
||||
test(
|
||||
'When machine-api server not found butt is disabled and shows the reason',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
const notFoundText = 'Machine API server was not discovered'
|
||||
await expect(page.getByText(notFoundText).first()).not.toBeVisible()
|
||||
|
||||
// Find the make button
|
||||
const makeButton = page.getByRole('button', { name: 'Make' })
|
||||
// Make sure the button is visible but disabled
|
||||
await expect(makeButton).toBeVisible()
|
||||
await expect(makeButton).toBeDisabled()
|
||||
|
||||
// When you hover over the button, the tooltip should show
|
||||
// that the machine-api server is not found
|
||||
await makeButton.hover()
|
||||
await expect(page.getByText(notFoundText).first()).toBeVisible()
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'When machine-api server not found home screen & project status shows the reason',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
const notFoundText = 'Machine API server was not discovered'
|
||||
|
||||
await expect(page.getByText(notFoundText)).not.toBeVisible()
|
||||
|
||||
const networkMachineToggle = page.getByTestId('network-machine-toggle')
|
||||
await networkMachineToggle.hover()
|
||||
await expect(page.getByText(notFoundText)).toBeVisible()
|
||||
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(page.getByText(notFoundText).nth(1)).not.toBeVisible()
|
||||
|
||||
await networkMachineToggle.hover()
|
||||
await expect(page.getByText(notFoundText).nth(1)).toBeVisible()
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
@ -347,6 +347,10 @@ test(
|
||||
'Restarting onboarding on desktop takes one attempt',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
|
@ -2,6 +2,7 @@ import { test, expect } from '@playwright/test'
|
||||
import {
|
||||
doExport,
|
||||
getUtils,
|
||||
isOutOfViewInScrollContainer,
|
||||
Paths,
|
||||
setupElectron,
|
||||
tearDown,
|
||||
@ -9,15 +10,45 @@ import {
|
||||
import fsp from 'fs/promises'
|
||||
import fs from 'fs'
|
||||
import { join } from 'path'
|
||||
import { FILE_EXT } from 'lib/constants'
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
})
|
||||
|
||||
test(
|
||||
'Can export from electron app',
|
||||
'click help/keybindings from home page',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
// click ? button
|
||||
await page.getByTestId('help-button').click()
|
||||
await expect(page.getByTestId('keybindings-button')).toBeVisible()
|
||||
// Click keyboard shortcuts button.
|
||||
await page.getByTestId('keybindings-button').click()
|
||||
// Make sure the keyboard shortcuts modal is visible.
|
||||
await expect(page.getByText('Enter Sketch Mode')).toBeVisible()
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'click help/keybindings from project page',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
@ -30,82 +61,181 @@ test(
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const u = await getUtils(page)
|
||||
|
||||
page.on('console', console.log)
|
||||
await electronApp.context().addInitScript(async () => {
|
||||
;(window as any).playwrightSkipFilePicker = true
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
// expect to see the text bracket
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
const pointOnModel = { x: 630, y: 280 }
|
||||
|
||||
await test.step('Opening the bracket project should load the stream', async () => {
|
||||
// expect to see the text bracket
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
})
|
||||
|
||||
const exportLocations: Array<Paths> = []
|
||||
await test.step('export the model as a glTF', async () => {
|
||||
exportLocations.push(
|
||||
await doExport(
|
||||
{
|
||||
type: 'gltf',
|
||||
storage: 'embedded',
|
||||
presentation: 'pretty',
|
||||
},
|
||||
page,
|
||||
true
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
await test.step('Check the export size', async () => {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
try {
|
||||
const outputGltf = await fsp.readFile('output.gltf')
|
||||
return outputGltf.byteLength
|
||||
} catch (e) {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBe(477327)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
})
|
||||
// click ? button
|
||||
await page.getByTestId('help-button').click()
|
||||
await expect(page.getByTestId('keybindings-button')).toBeVisible()
|
||||
// Click keyboard shortcuts button.
|
||||
await page.getByTestId('keybindings-button').click()
|
||||
// Make sure the keyboard shortcuts modal is visible.
|
||||
await expect(page.getByText('Enter Sketch Mode')).toBeVisible()
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'when code with error first loads you get errors in console',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/broken-code`, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/broken-code-test.kcl',
|
||||
`${dir}/broken-code/main.kcl`
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await expect(page.getByText('broken-code')).toBeVisible()
|
||||
|
||||
await page.getByText('broken-code').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// error in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
const crypticErrorText = `Expected a tag declarator`
|
||||
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test.describe('Can export from electron app', () => {
|
||||
const exportMethods = ['sidebarButton', 'commandBar'] as const
|
||||
|
||||
for (const method of exportMethods) {
|
||||
test(
|
||||
`Can export using ${method}`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const u = await getUtils(page)
|
||||
|
||||
page.on('console', console.log)
|
||||
await electronApp.context().addInitScript(async () => {
|
||||
;(window as any).playwrightSkipFilePicker = true
|
||||
})
|
||||
|
||||
const pointOnModel = { x: 630, y: 280 }
|
||||
|
||||
await test.step('Opening the bracket project should load the stream', async () => {
|
||||
// expect to see the text bracket
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [75, 75, 75]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(10)
|
||||
})
|
||||
|
||||
const exportLocations: Array<Paths> = []
|
||||
await test.step(`export the model as a glTF using ${method}`, async () => {
|
||||
exportLocations.push(
|
||||
await doExport(
|
||||
{
|
||||
type: 'gltf',
|
||||
storage: 'embedded',
|
||||
presentation: 'pretty',
|
||||
},
|
||||
page,
|
||||
method
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
await test.step('Check the export size', async () => {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
try {
|
||||
const outputGltf = await fsp.readFile('output.gltf')
|
||||
return outputGltf.byteLength
|
||||
} catch (e) {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBe(477327)
|
||||
|
||||
// clean up output.gltf
|
||||
await fsp.rm('output.gltf')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
test(
|
||||
'Rename and delete projects, also spam arrow keys when renaming',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
@ -394,6 +524,10 @@ test(
|
||||
'Deleting projects, can delete individual project, can still create projects after deleting all',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
})
|
||||
@ -488,6 +622,10 @@ test(
|
||||
'Can sort projects on home page',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
})
|
||||
@ -610,6 +748,10 @@ test(
|
||||
'When the project folder is empty, user can create new project and open it.',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({ testInfo })
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
@ -694,6 +836,10 @@ test(
|
||||
'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
@ -907,6 +1053,10 @@ test(
|
||||
'Search projects on desktop home',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const projectData = [
|
||||
['basic bracket', 'focusrite_scarlett_mounting_braket.kcl'],
|
||||
['basic-cube', 'basic_fillet_cube_end.kcl'],
|
||||
@ -964,10 +1114,198 @@ test(
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'file pane is scrollable when there are many files',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/testProject`, { recursive: true })
|
||||
const fileNames = [
|
||||
'angled_line.kcl',
|
||||
'basic_fillet_cube_close_opposite.kcl',
|
||||
'basic_fillet_cube_end.kcl',
|
||||
'basic_fillet_cube_next_adjacent.kcl',
|
||||
'basic_fillet_cube_previous_adjacent.kcl',
|
||||
'basic_fillet_cube_start.kcl',
|
||||
'big_number_angle_to_match_length_x.kcl',
|
||||
'big_number_angle_to_match_length_y.kcl',
|
||||
'close_arc.kcl',
|
||||
'computed_var.kcl',
|
||||
'cube-embedded.gltf',
|
||||
'cube.bin',
|
||||
'cube.glb',
|
||||
'cube.gltf',
|
||||
'cube.kcl',
|
||||
'cube.mtl',
|
||||
'cube.obj',
|
||||
'cylinder.kcl',
|
||||
'dimensions_match.kcl',
|
||||
'extrude-custom-plane.kcl',
|
||||
'extrude-inside-fn-with-tags.kcl',
|
||||
'fillet-and-shell.kcl',
|
||||
'fillet_duplicate_tags.kcl',
|
||||
'focusrite_scarlett_mounting_braket.kcl',
|
||||
'function_sketch.kcl',
|
||||
'function_sketch_with_position.kcl',
|
||||
'global-tags.kcl',
|
||||
'helix_ccw.kcl',
|
||||
'helix_defaults.kcl',
|
||||
'helix_defaults_negative_extrude.kcl',
|
||||
'helix_with_length.kcl',
|
||||
'i_shape.kcl',
|
||||
'kittycad_svg.kcl',
|
||||
'lego.kcl',
|
||||
'math.kcl',
|
||||
'member_expression_sketch_group.kcl',
|
||||
'mike_stress_test.kcl',
|
||||
'negative_args.kcl',
|
||||
'order-sketch-extrude-in-order.kcl',
|
||||
'order-sketch-extrude-out-of-order.kcl',
|
||||
'parametric.kcl',
|
||||
'parametric_with_tan_arc.kcl',
|
||||
'pattern_vase.kcl',
|
||||
'pentagon_fillet_sugar.kcl',
|
||||
'pipe_as_arg.kcl',
|
||||
'pipes_on_pipes.kcl',
|
||||
'riddle.kcl',
|
||||
'riddle_small.kcl',
|
||||
'router-template-slate.kcl',
|
||||
'scoped-tags.kcl',
|
||||
'server-rack-heavy.kcl',
|
||||
'server-rack-lite.kcl',
|
||||
'sketch_on_face.kcl',
|
||||
'sketch_on_face_circle_tagged.kcl',
|
||||
'sketch_on_face_end.kcl',
|
||||
'sketch_on_face_end_negative_extrude.kcl',
|
||||
'sketch_on_face_start.kcl',
|
||||
'tan_arc_x_line.kcl',
|
||||
'tangential_arc.kcl',
|
||||
]
|
||||
for (const fileName of fileNames) {
|
||||
await fsp.copyFile(
|
||||
`src/wasm-lib/tests/executor/inputs/${fileName}`,
|
||||
`${dir}/testProject/${fileName}`
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
await test.step('setup, open file pane', async () => {
|
||||
await page.getByText('testProject').click()
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await page.getByTestId('files-pane-button').click()
|
||||
})
|
||||
|
||||
await test.step('check the last file is out of view initially, and can be scrolled to', async () => {
|
||||
const element = page.getByText('tangential_arc.kcl')
|
||||
const container = page.getByTestId('file-pane-scroll-container')
|
||||
|
||||
await expect(await isOutOfViewInScrollContainer(element, container)).toBe(
|
||||
true
|
||||
)
|
||||
await element.scrollIntoViewIfNeeded()
|
||||
await expect(await isOutOfViewInScrollContainer(element, container)).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'select all in code editor does not actually select all, just what is visible (regression)',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
// src/wasm-lib/tests/executor/inputs/mike_stress_test.kcl
|
||||
const name = 'mike_stress_test'
|
||||
await fsp.mkdir(`${dir}/${name}`, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
`src/wasm-lib/tests/executor/inputs/${name}.kcl`,
|
||||
`${dir}/${name}/main.kcl`
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
await page.getByText('mike_stress_test').click()
|
||||
|
||||
const modifier =
|
||||
process.platform === 'win32' || process.platform === 'linux'
|
||||
? 'Control'
|
||||
: 'Meta'
|
||||
|
||||
await test.step('select all in code editor, check its length', async () => {
|
||||
await u.codeLocator.click()
|
||||
// expect u.codeLocator to have some text
|
||||
await expect(u.codeLocator).toContainText('line(')
|
||||
await page.keyboard.down(modifier)
|
||||
await page.keyboard.press('KeyA')
|
||||
await page.keyboard.up(modifier)
|
||||
|
||||
// check the length of the selected text
|
||||
const selectedText = await page.evaluate(() => {
|
||||
const selection = window.getSelection()
|
||||
return selection ? selection.toString() : ''
|
||||
})
|
||||
// even though if the user copied the text into their clipboard they would get the full text
|
||||
// it seems that the selection is limited to what is visible
|
||||
// we just want to check we did select something, and later we've verify it's empty
|
||||
expect(selectedText.length).toBeGreaterThan(10)
|
||||
})
|
||||
|
||||
await test.step('delete all the text, select again and verify there are no characters left', async () => {
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
await page.keyboard.down(modifier)
|
||||
await page.keyboard.press('KeyA')
|
||||
await page.keyboard.up(modifier)
|
||||
|
||||
// check the length of the selected text
|
||||
const selectedText = await page.evaluate(() => {
|
||||
const selection = window.getSelection()
|
||||
return selection ? selection.toString() : ''
|
||||
})
|
||||
expect(selectedText.length).toBe(0)
|
||||
await expect(u.codeLocator).toHaveText('')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Settings persist across restarts',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
await test.step('We can change a user setting like theme', async () => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
@ -1006,3 +1344,369 @@ test(
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.describe('Renaming in the file tree', () => {
|
||||
test(
|
||||
'A file you have open',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
const exampleDir = join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs'
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const fileToRename = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
|
||||
const renamedFile = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'newFileName.kcl' }) })
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const renameInput = page.getByPlaceholder('fileToRename.kcl')
|
||||
const newFileName = 'newFileName'
|
||||
const codeLocator = page.locator('.cm-content')
|
||||
|
||||
await test.step('Open project and file pane', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(fileToRename).toBeVisible()
|
||||
await fileToRename.click()
|
||||
await expect(projectMenuButton).toContainText('fileToRename.kcl')
|
||||
await u.openKclCodePanel()
|
||||
await expect(codeLocator).toContainText('circle(')
|
||||
await u.closeKclCodePanel()
|
||||
})
|
||||
|
||||
await test.step('Rename the file', async () => {
|
||||
await fileToRename.click({ button: 'right' })
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFileName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the file is renamed', async () => {
|
||||
await expect(fileToRename).not.toBeAttached()
|
||||
await expect(renamedFile).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Verify we navigated', async () => {
|
||||
await expect(projectMenuButton).toContainText(newFileName + FILE_EXT)
|
||||
const url = page.url()
|
||||
expect(url).toContain(newFileName)
|
||||
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
|
||||
await expect(projectMenuButton).not.toContainText('main.kcl')
|
||||
expect(url).not.toContain('fileToRename.kcl')
|
||||
expect(url).not.toContain('main.kcl')
|
||||
|
||||
await u.openKclCodePanel()
|
||||
await expect(codeLocator).toContainText('circle(')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'A file you do not have open',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
const exampleDir = join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs'
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const newFileName = 'newFileName'
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const fileToRename = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
|
||||
const renamedFile = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: newFileName + FILE_EXT }),
|
||||
})
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const renameInput = page.getByPlaceholder('fileToRename.kcl')
|
||||
const codeLocator = page.locator('.cm-content')
|
||||
|
||||
await test.step('Open project and file pane', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(fileToRename).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Rename the file', async () => {
|
||||
await fileToRename.click({ button: 'right' })
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFileName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the file is renamed', async () => {
|
||||
await expect(fileToRename).not.toBeAttached()
|
||||
await expect(renamedFile).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Verify we have not navigated', async () => {
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await expect(projectMenuButton).not.toContainText(
|
||||
newFileName + FILE_EXT
|
||||
)
|
||||
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
|
||||
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain(newFileName)
|
||||
expect(url).not.toContain('fileToRename.kcl')
|
||||
|
||||
await u.openKclCodePanel()
|
||||
await expect(codeLocator).toContainText('fillet(')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`A folder you're not inside`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
|
||||
recursive: true,
|
||||
})
|
||||
const exampleDir = join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs'
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const folderToRename = page.getByRole('button', {
|
||||
name: 'folderToRename',
|
||||
})
|
||||
const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const renameInput = page.getByPlaceholder('folderToRename')
|
||||
const newFolderName = 'newFolderName'
|
||||
|
||||
await test.step('Open project and file pane', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain('folderToRename')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(folderToRename).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Rename the folder', async () => {
|
||||
await folderToRename.click({ button: 'right' })
|
||||
await expect(renameMenuItem).toBeVisible()
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFolderName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the folder is renamed, and no navigation occurred', async () => {
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain('folderToRename')
|
||||
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await expect(renamedFolder).toBeVisible()
|
||||
await expect(folderToRename).not.toBeAttached()
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`A folder you are inside`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const exampleDir = join('src', 'wasm-lib', 'tests', 'executor', 'inputs')
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
|
||||
recursive: true,
|
||||
})
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const folderToRename = page.getByRole('button', {
|
||||
name: 'folderToRename',
|
||||
})
|
||||
const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
|
||||
const fileWithinFolder = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
|
||||
})
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const renameInput = page.getByPlaceholder('folderToRename')
|
||||
const newFolderName = 'newFolderName'
|
||||
|
||||
await test.step('Open project and navigate into folder', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain('folderToRename')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(folderToRename).toBeVisible()
|
||||
await folderToRename.click()
|
||||
await expect(fileWithinFolder).toBeVisible()
|
||||
await fileWithinFolder.click()
|
||||
|
||||
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
|
||||
const newUrl = page.url()
|
||||
expect(newUrl).toContain('folderToRename')
|
||||
expect(newUrl).toContain('someFileWithin.kcl')
|
||||
expect(newUrl).not.toContain('main.kcl')
|
||||
})
|
||||
|
||||
await test.step('Rename the folder', async () => {
|
||||
await folderToRename.click({ button: 'right' })
|
||||
await expect(renameMenuItem).toBeVisible()
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFolderName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the folder is renamed, and navigated to new path', async () => {
|
||||
const urlSnippet = encodeURIComponent(
|
||||
join(newFolderName, 'someFileWithin.kcl')
|
||||
)
|
||||
await page.waitForURL(new RegExp(urlSnippet))
|
||||
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
|
||||
await expect(renamedFolder).toBeVisible()
|
||||
await expect(folderToRename).not.toBeAttached()
|
||||
|
||||
// URL is synchronous, so we check the other stuff first
|
||||
const url = page.url()
|
||||
expect(url).not.toContain('main.kcl')
|
||||
expect(url).toContain(newFolderName)
|
||||
expect(url).toContain('someFileWithin.kcl')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -194,7 +194,7 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(page.getByText('Unexpected token').first()).toBeVisible()
|
||||
await expect(page.getByText('Unexpected token: |').first()).toBeVisible()
|
||||
|
||||
// Okay execution finished, let's start editing text below the error.
|
||||
await u.codeLocator.click()
|
||||
@ -236,9 +236,13 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async (code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
}, TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR)
|
||||
await page.addInitScript(
|
||||
async ({ code }) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
;(window as any).playwrightSkipFilePicker = true
|
||||
},
|
||||
{ code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR }
|
||||
)
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
|
||||
@ -325,7 +329,7 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
|
||||
// Expect it to succeed.
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
await expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 })
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
|
||||
@ -337,6 +341,7 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
}) => {
|
||||
// This is being weird on ubuntu and windows.
|
||||
test.skip(
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
process.platform === 'linux' || process.platform === 'win32',
|
||||
'This test is being weird on ubuntu'
|
||||
)
|
||||
@ -420,6 +425,10 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
`Network health indicator only appears in modeling view`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
@ -472,7 +481,7 @@ async function clickExportButton(page: Page) {
|
||||
// Click the export button
|
||||
await exportButton.click()
|
||||
|
||||
// Click the stl.
|
||||
// Click the gltf.
|
||||
const gltfOption = page.getByRole('option', { name: 'glTF' })
|
||||
await expect(gltfOption).toBeVisible()
|
||||
|
||||
|
@ -344,111 +344,108 @@ test.describe('Sketch tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// failing for the same reason as "Can undo a sketch modification with ctrl+z"
|
||||
// please fix together
|
||||
test.fixme(
|
||||
'Can edit a sketch that has been extruded in the same pipe',
|
||||
async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([4.61, -14.01], %)
|
||||
test('Can edit a sketch that has been extruded in the same pipe', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([4.61, -10.01], %)
|
||||
|> line([12.73, -0.09], %)
|
||||
|> tangentialArcTo([24.95, -5.38], %)
|
||||
|> tangentialArcTo([24.95, -0.38], %)
|
||||
|> close(%)
|
||||
|> extrude(5, %)`
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.sendCustomCmd({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
vantage: { x: 0, y: -1250, z: 580 },
|
||||
center: { x: 0, y: 0, z: 0 },
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
},
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
await u.sendCustomCmd({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
await page.waitForTimeout(100)
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.sendCustomCmd({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
vantage: { x: 0, y: -1250, z: 580 },
|
||||
center: { x: 0, y: 0, z: 0 },
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
},
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
await u.sendCustomCmd({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const startPX = [665, 458]
|
||||
const startPX = [665, 397]
|
||||
|
||||
const dragPX = 40
|
||||
const dragPX = 40
|
||||
|
||||
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(400)
|
||||
let prevContent = await page.locator('.cm-content').innerText()
|
||||
await page.getByText('startProfileAt([4.61, -10.01], %)').click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(400)
|
||||
let prevContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
||||
|
||||
// drag startProfieAt handle
|
||||
await page.dragAndDrop('#stream', '#stream', {
|
||||
sourcePosition: { x: startPX[0], y: startPX[1] },
|
||||
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
prevContent = await page.locator('.cm-content').innerText()
|
||||
// drag startProfieAt handle
|
||||
await page.dragAndDrop('#stream', '#stream', {
|
||||
sourcePosition: { x: startPX[0], y: startPX[1] },
|
||||
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
prevContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
// drag line handle
|
||||
await page.waitForTimeout(100)
|
||||
// drag line handle
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
||||
await page.waitForTimeout(100)
|
||||
await page.dragAndDrop('#stream', '#stream', {
|
||||
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
|
||||
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX },
|
||||
})
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
prevContent = await page.locator('.cm-content').innerText()
|
||||
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
||||
await page.waitForTimeout(100)
|
||||
await page.dragAndDrop('#stream', '#stream', {
|
||||
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
|
||||
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX },
|
||||
})
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
prevContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
// drag tangentialArcTo handle
|
||||
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
|
||||
await page.dragAndDrop('#stream', '#stream', {
|
||||
sourcePosition: { x: tangentEnd.x, y: tangentEnd.y - 5 },
|
||||
targetPosition: {
|
||||
x: tangentEnd.x + dragPX,
|
||||
y: tangentEnd.y + dragPX,
|
||||
},
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
// drag tangentialArcTo handle
|
||||
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
|
||||
await page.dragAndDrop('#stream', '#stream', {
|
||||
sourcePosition: { x: tangentEnd.x + 10, y: tangentEnd.y - 5 },
|
||||
targetPosition: {
|
||||
x: tangentEnd.x + dragPX,
|
||||
y: tangentEnd.y + dragPX,
|
||||
},
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
|
||||
// expect the code to have changed
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([7.12, -16.82], %)
|
||||
|> line([15.4, -2.74], %)
|
||||
|> tangentialArcTo([24.95, -5.38], %)
|
||||
|> line([2.65, -2.69], %)
|
||||
|> close(%)
|
||||
|> extrude(5, %)`)
|
||||
}
|
||||
)
|
||||
// expect the code to have changed
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([7.12, -12.68], %)
|
||||
|> line([15.39, -2.78], %)
|
||||
|> tangentialArcTo([27.6, -3.05], %)
|
||||
|> close(%)
|
||||
|> extrude(5, %)
|
||||
`)
|
||||
})
|
||||
|
||||
test('Can edit a sketch that has been revolved in the same pipe', async ({
|
||||
page,
|
||||
@ -602,7 +599,7 @@ test.describe('Sketch tests', () => {
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
// exit the sketch, reset relative clicker
|
||||
click00r(undefined, undefined)
|
||||
await click00r(undefined, undefined)
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
|
@ -53,6 +53,7 @@ test(
|
||||
async ({ page, context }) => {
|
||||
// skip on macos and windows.
|
||||
test.skip(
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
process.platform === 'darwin' || process.platform === 'win32',
|
||||
'Skip on macos and windows'
|
||||
)
|
||||
@ -963,3 +964,69 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('theme persists', async ({ page, context }) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, 20], %)
|
||||
|> line([-20, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(10, %)
|
||||
`
|
||||
)
|
||||
}, KCL_DEFAULT_LENGTH)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// await page.getByRole('link', { name: 'Settings Settings (tooltip)' }).click()
|
||||
await expect(page.getByTestId('settings-link')).toBeVisible()
|
||||
await page.getByTestId('settings-link').click()
|
||||
|
||||
// open user settingns
|
||||
await page.getByRole('radio', { name: 'person User' }).click()
|
||||
|
||||
await page.getByTestId('app-theme').selectOption('light')
|
||||
|
||||
await page.getByTestId('settings-close-button').click()
|
||||
|
||||
const networkToggle = page.getByTestId('network-toggle')
|
||||
|
||||
// simulate network down
|
||||
await u.emulateNetworkConditions({
|
||||
offline: true,
|
||||
// values of 0 remove any active throttling. crbug.com/456324#c9
|
||||
latency: 0,
|
||||
downloadThroughput: -1,
|
||||
uploadThroughput: -1,
|
||||
})
|
||||
|
||||
// Disconnect and reconnect to check the theme persists through a reload
|
||||
|
||||
// Expect the network to be down
|
||||
await expect(networkToggle).toContainText('Offline')
|
||||
|
||||
// simulate network up
|
||||
await u.emulateNetworkConditions({
|
||||
offline: false,
|
||||
// values of 0 remove any active throttling. crbug.com/456324#c9
|
||||
latency: 0,
|
||||
downloadThroughput: -1,
|
||||
uploadThroughput: -1,
|
||||
})
|
||||
|
||||
await expect(networkToggle).toContainText('Connected')
|
||||
|
||||
await expect(page.getByText('building scene')).not.toBeVisible()
|
||||
|
||||
await expect(page, 'expect screenshot to have light theme').toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@ -5,6 +5,7 @@ import {
|
||||
TestInfo,
|
||||
BrowserContext,
|
||||
_electron as electron,
|
||||
Locator,
|
||||
} from '@playwright/test'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import os from 'os'
|
||||
@ -94,6 +95,8 @@ async function expectCmdLog(page: Page, locatorStr: string, timeout = 5000) {
|
||||
await expect(page.locator(locatorStr).last()).toBeVisible({ timeout })
|
||||
}
|
||||
|
||||
// Ignoring the lint since I assume someone will want to use this for a test.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function waitForDefaultPlanesToBeVisible(page: Page) {
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
@ -102,17 +105,21 @@ async function waitForDefaultPlanesToBeVisible(page: Page) {
|
||||
)
|
||||
}
|
||||
|
||||
async function openKclCodePanel(page: Page) {
|
||||
const paneLocator = page.getByTestId('code-pane-button')
|
||||
const ariaSelected = await paneLocator?.getAttribute('aria-pressed')
|
||||
const isOpen = ariaSelected === 'true'
|
||||
async function openPane(page: Page, testId: string) {
|
||||
const locator = page.getByTestId(testId)
|
||||
await expect(locator).toBeVisible()
|
||||
const isOpen = (await locator?.getAttribute('aria-pressed')) === 'true'
|
||||
|
||||
if (!isOpen) {
|
||||
await paneLocator.click()
|
||||
await expect(paneLocator).toHaveAttribute('aria-pressed', 'true')
|
||||
await locator.click()
|
||||
await expect(locator).toHaveAttribute('aria-pressed', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
async function openKclCodePanel(page: Page) {
|
||||
await openPane(page, 'code-pane-button')
|
||||
}
|
||||
|
||||
async function closeKclCodePanel(page: Page) {
|
||||
const paneLocator = page.getByTestId('code-pane-button')
|
||||
const ariaSelected = await paneLocator?.getAttribute('aria-pressed')
|
||||
@ -125,14 +132,7 @@ async function closeKclCodePanel(page: Page) {
|
||||
}
|
||||
|
||||
async function openDebugPanel(page: Page) {
|
||||
const debugLocator = page.getByTestId('debug-pane-button')
|
||||
await expect(debugLocator).toBeVisible()
|
||||
const isOpen = (await debugLocator?.getAttribute('aria-pressed')) === 'true'
|
||||
|
||||
if (!isOpen) {
|
||||
await debugLocator.click()
|
||||
await expect(debugLocator).toHaveAttribute('aria-pressed', 'true')
|
||||
}
|
||||
await openPane(page, 'debug-pane-button')
|
||||
}
|
||||
|
||||
async function closeDebugPanel(page: Page) {
|
||||
@ -145,6 +145,28 @@ async function closeDebugPanel(page: Page) {
|
||||
}
|
||||
}
|
||||
|
||||
async function openFilePanel(page: Page) {
|
||||
await openPane(page, 'files-pane-button')
|
||||
}
|
||||
|
||||
async function closeFilePanel(page: Page) {
|
||||
const fileLocator = page.getByTestId('files-pane-button')
|
||||
await expect(fileLocator).toBeVisible()
|
||||
const isOpen = (await fileLocator?.getAttribute('aria-pressed')) === 'true'
|
||||
if (isOpen) {
|
||||
await fileLocator.click()
|
||||
await expect(fileLocator).not.toHaveAttribute('aria-pressed', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
async function openVariablesPane(page: Page) {
|
||||
await openPane(page, 'variables-pane-button')
|
||||
}
|
||||
|
||||
async function openLogsPane(page: Page) {
|
||||
await openPane(page, 'logs-pane-button')
|
||||
}
|
||||
|
||||
async function waitForCmdReceive(page: Page, commandType: string) {
|
||||
return page
|
||||
.locator(`[data-receive-command-type="${commandType}"]`)
|
||||
@ -171,7 +193,8 @@ export const wiggleMove = async (
|
||||
const isElVis = await page.locator(locator).isVisible()
|
||||
if (isElVis) return
|
||||
}
|
||||
const [x1, y1] = [0, Math.sin((tau / steps) * j * freq) * amplitude]
|
||||
// x1 is 0.
|
||||
const y1 = 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,
|
||||
@ -317,6 +340,10 @@ export async function getUtils(page: Page) {
|
||||
closeKclCodePanel: () => closeKclCodePanel(page),
|
||||
openDebugPanel: () => openDebugPanel(page),
|
||||
closeDebugPanel: () => closeDebugPanel(page),
|
||||
openFilePanel: () => openFilePanel(page),
|
||||
closeFilePanel: () => closeFilePanel(page),
|
||||
openVariablesPane: () => openVariablesPane(page),
|
||||
openLogsPane: () => openLogsPane(page),
|
||||
openAndClearDebugPanel: async () => {
|
||||
await openDebugPanel(page)
|
||||
return clearCommandLogs(page)
|
||||
@ -452,7 +479,10 @@ export async function getUtils(page: Page) {
|
||||
return page.evaluate('window.tearDown()')
|
||||
}
|
||||
|
||||
cdpSession?.send('Network.emulateNetworkConditions', networkOptions)
|
||||
return cdpSession?.send(
|
||||
'Network.emulateNetworkConditions',
|
||||
networkOptions
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -539,17 +569,34 @@ export interface Paths {
|
||||
export const doExport = async (
|
||||
output: Models['OutputFormat_type'],
|
||||
page: Page,
|
||||
isElectron = false
|
||||
exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown'
|
||||
): Promise<Paths> => {
|
||||
if (!isElectron) {
|
||||
if (exportFrom === 'dropdown') {
|
||||
await page.getByRole('button', { name: APP_NAME }).click()
|
||||
const exportMenuButton = page.getByRole('button', {
|
||||
name: 'Export current part',
|
||||
})
|
||||
await expect(exportMenuButton).toBeVisible()
|
||||
await exportMenuButton.click()
|
||||
} else {
|
||||
} else if (exportFrom === 'sidebarButton') {
|
||||
await expect(page.getByTestId('export-pane-button')).toBeVisible()
|
||||
await page.getByTestId('export-pane-button').click()
|
||||
} else if (exportFrom === 'commandBar') {
|
||||
const commandBarButton = page.getByRole('button', { name: 'Commands' })
|
||||
await expect(commandBarButton).toBeVisible()
|
||||
// Click the command bar button
|
||||
await commandBarButton.click()
|
||||
|
||||
// Wait for the command bar to appear
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
|
||||
const textToCadCommand = page.getByRole('option', {
|
||||
name: 'floppy disk arrow Export',
|
||||
})
|
||||
await expect(textToCadCommand.first()).toBeVisible()
|
||||
// Click the Text-to-CAD command
|
||||
await textToCadCommand.first().click()
|
||||
}
|
||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||
|
||||
@ -577,7 +624,7 @@ export const doExport = async (
|
||||
const [downloadPromise1, downloadResolve1] = getPromiseAndResolve()
|
||||
let downloadCnt = 0
|
||||
|
||||
if (!isElectron)
|
||||
if (exportFrom === 'dropdown')
|
||||
page.on('download', async (download) => {
|
||||
if (downloadCnt === 0) {
|
||||
downloadResolve1(download)
|
||||
@ -585,7 +632,7 @@ export const doExport = async (
|
||||
downloadCnt++
|
||||
})
|
||||
await page.getByRole('button', { name: 'Submit command' }).click()
|
||||
if (isElectron) {
|
||||
if (exportFrom === 'sidebarButton' || exportFrom === 'commandBar') {
|
||||
return {
|
||||
modelPath: '',
|
||||
imagePath: '',
|
||||
@ -649,6 +696,7 @@ export async function tearDown(page: Page, testInfo: TestInfo) {
|
||||
export async function setup(context: BrowserContext, page: Page) {
|
||||
await context.addInitScript(
|
||||
async ({ token, settingsKey, settings, IS_PLAYWRIGHT_KEY }) => {
|
||||
localStorage.clear()
|
||||
localStorage.setItem('TOKEN_PERSIST_KEY', token)
|
||||
localStorage.setItem('persistCode', ``)
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
@ -684,6 +732,8 @@ export async function setup(context: BrowserContext, page: Page) {
|
||||
])
|
||||
// kill animations, speeds up tests and reduced flakiness
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||
|
||||
await page.reload()
|
||||
}
|
||||
|
||||
export async function setupElectron({
|
||||
@ -745,3 +795,22 @@ export async function setupElectron({
|
||||
|
||||
return { electronApp, page }
|
||||
}
|
||||
|
||||
export async function isOutOfViewInScrollContainer(
|
||||
element: Locator,
|
||||
container: Locator
|
||||
): Promise<boolean> {
|
||||
const elementBox = await element.boundingBox({ timeout: 5_000 })
|
||||
const containerBox = await container.boundingBox({ timeout: 5_000 })
|
||||
|
||||
let isOutOfView = false
|
||||
if (elementBox && containerBox)
|
||||
return (
|
||||
elementBox.y + elementBox.height > containerBox.y + containerBox.height ||
|
||||
elementBox.y < containerBox.y ||
|
||||
elementBox.x + elementBox.width > containerBox.x + containerBox.width ||
|
||||
elementBox.x < containerBox.x
|
||||
)
|
||||
|
||||
return isOutOfView
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ test.describe('Testing constraints', () => {
|
||||
page.getByRole('button', { name: 'Exit Sketch' })
|
||||
).not.toBeVisible()
|
||||
})
|
||||
test(`Test remove constraints`, async ({ page }) => {
|
||||
test(`Remove constraints`, async ({ page }) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
|
@ -977,10 +977,6 @@ const part001 = startSketchOn('XZ')
|
||||
const hoverPos = { x: segmentToDelete.x, y: segmentToDelete.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(hoverPos.x, hoverPos.y)
|
||||
await wiggleMove(
|
||||
page,
|
||||
|
@ -4,7 +4,6 @@ import { getUtils, setup, setupElectron, tearDown } from './test-utils'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { TEST_SETTINGS_KEY, TEST_SETTINGS_CORRUPTED } from './storageStates'
|
||||
import * as TOML from '@iarna/toml'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
@ -116,8 +115,7 @@ test.describe('Testing settings', () => {
|
||||
).not.toBeChecked()
|
||||
})
|
||||
|
||||
// TODO fixme reset doesn't seem to work for color setting
|
||||
test.fixme('Project and user settings can be reset', async ({ page }) => {
|
||||
test('Project and user settings can be reset', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
@ -162,6 +160,11 @@ test.describe('Testing settings', () => {
|
||||
// Click the reset settings button.
|
||||
await resetButton.click()
|
||||
|
||||
await expect(page.getByText('Settings restored to default')).toBeVisible()
|
||||
await expect(
|
||||
page.getByText('Settings restored to default')
|
||||
).not.toBeVisible()
|
||||
|
||||
// Verify it is now set to the inherited user value
|
||||
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
||||
|
||||
@ -193,6 +196,10 @@ test.describe('Testing settings', () => {
|
||||
`Project settings override user settings on desktop`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
@ -205,7 +212,6 @@ test.describe('Testing settings', () => {
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const u = await getUtils(page)
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { test, expect, Page } from '@playwright/test'
|
||||
import * as fsp from 'fs/promises'
|
||||
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
|
||||
import { join } from 'path'
|
||||
import { getUtils, setup, tearDown } from './test-utils'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
@ -192,7 +190,8 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
|
||||
// Type the prompt.
|
||||
await page.keyboard.type('akjsndladf ghgsssswefiuwq22262664')
|
||||
const randomPrompt = `aslkdfja;` + Date.now() + `FFFFEIWJF`
|
||||
await page.keyboard.type(randomPrompt)
|
||||
await page.waitForTimeout(1000)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
|
34
e2e/playwright/user-sidebar-menu-tests.spec.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
import { setupElectron, tearDown } from './test-utils'
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
})
|
||||
|
||||
test.describe('Electron user sidebar menu tests', () => {
|
||||
test(
|
||||
'User settings has correct shortcut',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
// Open the user sidebar menu.
|
||||
await page.getByTestId('user-sidebar-toggle').click()
|
||||
|
||||
// No space after "User settings" since it's textContent.
|
||||
const text =
|
||||
process.platform === 'darwin' ? 'User settings⌘,' : 'User settingsCtrl,'
|
||||
const userSettingsButton = page.getByTestId('user-settings')
|
||||
await expect(userSettingsButton).toBeVisible()
|
||||
await expect(userSettingsButton).toHaveText(text)
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
})
|
@ -631,6 +631,7 @@
|
||||
"errorno": {
|
||||
"description": "The error number.",
|
||||
"format": "int64",
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"reason": {
|
||||
@ -661,6 +662,7 @@
|
||||
"tar_temp": {
|
||||
"description": "The target temperature.",
|
||||
"format": "int64",
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"target": {
|
||||
@ -671,10 +673,8 @@
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"errorno",
|
||||
"result",
|
||||
"sequence_id",
|
||||
"tar_temp",
|
||||
"target"
|
||||
],
|
||||
"type": "object"
|
||||
@ -1155,6 +1155,59 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"description": "A gcode file.",
|
||||
"properties": {
|
||||
"command": {
|
||||
"enum": [
|
||||
"gcode_file"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"param": {
|
||||
"description": "The param.",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"print_type": {
|
||||
"description": "The print type.",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Reason"
|
||||
}
|
||||
],
|
||||
"description": "The reason for the message."
|
||||
},
|
||||
"result": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Result"
|
||||
}
|
||||
],
|
||||
"description": "The result of the command."
|
||||
},
|
||||
"sequence_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SequenceId"
|
||||
}
|
||||
],
|
||||
"description": "The sequence id."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"reason",
|
||||
"result",
|
||||
"sequence_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"description": "Project file.",
|
||||
|
29
package.json
@ -19,7 +19,7 @@
|
||||
"@codemirror/search": "^6.5.6",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@csstools/postcss-oklab-function": "^3.0.16",
|
||||
"@csstools/postcss-oklab-function": "^4.0.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
@ -53,7 +53,7 @@
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-modal-promise": "^1.0.2",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"sketch-helpers": "^0.0.4",
|
||||
"three": "^0.166.1",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
@ -81,22 +81,23 @@
|
||||
"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",
|
||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||
"build:wasm": "(cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
|
||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm": "(cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
||||
"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": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||
"lint": "eslint --fix src",
|
||||
"lint": "eslint --fix src e2e",
|
||||
"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)\"",
|
||||
"make:dev": "make dev",
|
||||
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
|
||||
"electron:start": "electron-forge start",
|
||||
"electron:package": "electron-forge package",
|
||||
"electron:make": "electron-forge make",
|
||||
"electron:publish": "electron-forge publish",
|
||||
"electron:e2e:local": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron"
|
||||
"tron:start": "electron-forge start",
|
||||
"tron:package": "electron-forge package",
|
||||
"tron:make": "electron-forge make",
|
||||
"tron:publish": "electron-forge publish",
|
||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
@ -130,17 +131,17 @@
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@lezer/generator": "^1.7.1",
|
||||
"@playwright/test": "^1.45.1",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/electron": "^1.6.10",
|
||||
"@types/isomorphic-fetch": "^0.0.39",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "^18.19.31",
|
||||
"@types/node": "^22.4.2",
|
||||
"@types/pixelmatch": "^5.2.6",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/react": "^18.3.2",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
"@types/three": "^0.163.0",
|
||||
@ -156,7 +157,7 @@
|
||||
"@xstate/cli": "^0.5.17",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"d3-force": "^3.0.0",
|
||||
"electron": "^31.2.1",
|
||||
"electron": "^32.0.1",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
|
@ -185,6 +185,8 @@ export function Toolbar({
|
||||
maybeIconConfig[0].disabled
|
||||
}
|
||||
name={maybeIconConfig[0].title}
|
||||
// aria-description is still in ARIA 1.3 draft.
|
||||
// eslint-disable-next-line jsx-a11y/aria-props
|
||||
aria-description={maybeIconConfig[0].description}
|
||||
onClick={() =>
|
||||
maybeIconConfig[0].onClick(configCallbackProps)
|
||||
@ -225,6 +227,8 @@ export function Toolbar({
|
||||
(!itemConfig.showTitle ? ' !px-0' : '')
|
||||
}
|
||||
name={itemConfig.title}
|
||||
// aria-description is still in ARIA 1.3 draft.
|
||||
// eslint-disable-next-line jsx-a11y/aria-props
|
||||
aria-description={itemConfig.description}
|
||||
aria-pressed={itemConfig.isActive}
|
||||
disabled={
|
||||
|
@ -52,24 +52,21 @@ const DownloadAppBanner = () => {
|
||||
</a>{' '}
|
||||
to download the app for the best experience.
|
||||
</p>
|
||||
<p className="mt-6">
|
||||
If you're on Linux and the browser is your only way to use the app,
|
||||
you can permanently dismiss this banner by{' '}
|
||||
<a
|
||||
onClick={() => {
|
||||
setIsBannerDismissed(true)
|
||||
settings.send({
|
||||
type: 'set.app.dismissWebBanner',
|
||||
data: { level: 'user', value: true },
|
||||
})
|
||||
}}
|
||||
href="/"
|
||||
className="!text-warn-80 dark:!text-warn-80 dark:hover:!text-warn-70 underline"
|
||||
>
|
||||
toggling the App > Dismiss Web Banner setting
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
{!navigator?.userAgent.includes('Chrome') && (
|
||||
<p className="mt-6">
|
||||
If you want to stay here on the web-app, we currently only support
|
||||
Chrome. Please use{' '}
|
||||
<a
|
||||
href="https://www.google.com/chrome/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="!text-warn-80 dark:!text-warn-80 dark:hover:!text-warn-70 underline"
|
||||
>
|
||||
this link
|
||||
</a>{' '}
|
||||
to download it.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
|
@ -153,33 +153,34 @@ export const FileMachineProvider = ({
|
||||
event: EventFrom<typeof fileMachine, 'Rename file'>
|
||||
) => {
|
||||
const { oldName, newName, isDir } = event.data
|
||||
const name = newName ? newName : DEFAULT_FILE_NAME
|
||||
const name = newName
|
||||
? newName.endsWith(FILE_EXT) || isDir
|
||||
? newName
|
||||
: newName + FILE_EXT
|
||||
: DEFAULT_FILE_NAME
|
||||
const oldPath = window.electron.path.join(
|
||||
context.selectedDirectory.path,
|
||||
oldName
|
||||
)
|
||||
const newDirPath = window.electron.path.join(
|
||||
const newPath = window.electron.path.join(
|
||||
context.selectedDirectory.path,
|
||||
name
|
||||
)
|
||||
const newPath =
|
||||
newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
|
||||
|
||||
await window.electron.rename(oldPath, newPath)
|
||||
window.electron.rename(oldPath, newPath)
|
||||
|
||||
if (!file) {
|
||||
return Promise.reject(new Error('file is not defined'))
|
||||
}
|
||||
|
||||
const currentFilePath = window.electron.path.join(file.path, file.name)
|
||||
if (oldPath === currentFilePath && project?.path) {
|
||||
if (oldPath === file.path && project?.path) {
|
||||
// If we just renamed the current file, navigate to the new path
|
||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
|
||||
} else if (file?.path.includes(oldPath)) {
|
||||
// If we just renamed a directory that the current file is in, navigate to the new path
|
||||
navigate(
|
||||
`..${PATHS.FILE}/${encodeURIComponent(
|
||||
file.path.replace(oldPath, newDirPath)
|
||||
file.path.replace(oldPath, newPath)
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
@ -464,7 +464,10 @@ export const FileTreeInner = ({
|
||||
}, [documentHasFocus])
|
||||
|
||||
return (
|
||||
<div className="overflow-auto pb-12 absolute inset-0">
|
||||
<div
|
||||
className="overflow-auto pb-12 absolute inset-0"
|
||||
data-testid="file-pane-scroll-container"
|
||||
>
|
||||
<ul
|
||||
className="m-0 p-0 text-sm"
|
||||
onClickCapture={(e) => {
|
||||
|
@ -23,7 +23,10 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Popover.Button className="grid p-0 m-0 border-none rounded-full place-content-center">
|
||||
<Popover.Button
|
||||
className="grid p-0 m-0 border-none rounded-full place-content-center"
|
||||
data-testid="help-button"
|
||||
>
|
||||
<CustomIcon
|
||||
name="questionMark"
|
||||
className="rounded-full w-7 h-7 bg-chalkboard-110 dark:bg-chalkboard-80 text-chalkboard-10"
|
||||
@ -95,6 +98,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
||||
: PATHS.HOME + PATHS.SETTINGS_KEYBINDINGS
|
||||
navigate(targetPath)
|
||||
}}
|
||||
data-testid="keybindings-button"
|
||||
>
|
||||
Keyboard shortcuts
|
||||
</HelpMenuItem>
|
||||
|
@ -11,7 +11,6 @@ import toast from 'react-hot-toast'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import openWindow from 'lib/openWindow'
|
||||
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
|
||||
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
|
||||
|
||||
export function LowerRightControls({
|
||||
children,
|
||||
@ -25,8 +24,6 @@ export function LowerRightControls({
|
||||
const linkOverrideClassName =
|
||||
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
|
||||
|
||||
const isTestEnv = window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true'
|
||||
|
||||
async function reportbug(event: {
|
||||
preventDefault: () => void
|
||||
stopPropagation: () => void
|
||||
@ -74,7 +71,7 @@ export function LowerRightControls({
|
||||
rel="noopener noreferrer"
|
||||
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
|
||||
>
|
||||
v{isTestEnv ? '11.22.33' : APP_VERSION}
|
||||
v{APP_VERSION}
|
||||
</a>
|
||||
<a
|
||||
onClick={reportbug}
|
||||
@ -96,6 +93,7 @@ export function LowerRightControls({
|
||||
? filePath + PATHS.SETTINGS + '?tab=project'
|
||||
: PATHS.HOME + PATHS.SETTINGS
|
||||
}
|
||||
data-testid="settings-link"
|
||||
>
|
||||
<CustomIcon
|
||||
name="settings"
|
||||
|
@ -63,8 +63,10 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
data: { name: 'Make', groupId: 'modeling' },
|
||||
})
|
||||
},
|
||||
hide: () => machineManager.machineCount() === 0,
|
||||
hideOnPlatform: 'web',
|
||||
hide: () => !isDesktop(),
|
||||
disable: () => {
|
||||
return machineManager.noMachinesReason()
|
||||
},
|
||||
},
|
||||
]
|
||||
const filteredActions: SidebarAction[] = sidebarActions.filter(
|
||||
@ -186,6 +188,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
iconSize: 'md',
|
||||
}}
|
||||
onClick={action.action}
|
||||
disabledText={action.disable?.()}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
@ -238,6 +241,7 @@ interface ModelingPaneButtonProps
|
||||
onClick: () => void
|
||||
paneIsOpen?: boolean
|
||||
showBadge?: BadgeInfoComputed
|
||||
disabledText?: string
|
||||
}
|
||||
|
||||
function ModelingPaneButton({
|
||||
@ -245,6 +249,7 @@ function ModelingPaneButton({
|
||||
onClick,
|
||||
paneIsOpen,
|
||||
showBadge,
|
||||
disabledText,
|
||||
...props
|
||||
}: ModelingPaneButtonProps) {
|
||||
useHotkeys(paneConfig.keybinding, onClick, {
|
||||
@ -258,6 +263,8 @@ function ModelingPaneButton({
|
||||
onClick={onClick}
|
||||
name={paneConfig.title}
|
||||
data-testid={paneConfig.id + '-pane-button'}
|
||||
disabled={disabledText !== undefined}
|
||||
aria-disabled={disabledText !== undefined}
|
||||
{...props}
|
||||
>
|
||||
<ActionIcon
|
||||
@ -284,6 +291,7 @@ function ModelingPaneButton({
|
||||
>
|
||||
<span className="flex-1">
|
||||
{paneConfig.title}
|
||||
{disabledText !== undefined ? ` (${disabledText})` : ''}
|
||||
{paneIsOpen !== undefined ? ` pane` : ''}
|
||||
</span>
|
||||
<kbd className="hotkey text-xs capitalize">
|
||||
@ -326,4 +334,5 @@ export type SidebarAction = {
|
||||
action: () => void
|
||||
hideOnPlatform?: 'desktop' | 'web'
|
||||
hide?: boolean | (() => boolean)
|
||||
disable?: () => string | undefined
|
||||
}
|
||||
|
@ -9,7 +9,9 @@ export const NetworkMachineIndicator = ({
|
||||
}: {
|
||||
className?: string
|
||||
}) => {
|
||||
const machineCount = Object.keys(machineManager.machines).length
|
||||
const machineCount = machineManager.machineCount()
|
||||
const reason = machineManager.noMachinesReason()
|
||||
|
||||
return isDesktop() ? (
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
@ -26,7 +28,7 @@ export const NetworkMachineIndicator = ({
|
||||
</p>
|
||||
)}
|
||||
<Tooltip position="top-right" wrapperClassName="ui-open:hidden">
|
||||
Network machines ({machineCount})
|
||||
Network machines ({machineCount}) {reason && `: ${reason}`}
|
||||
</Tooltip>
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
|
@ -7,12 +7,29 @@ export default function usePlatform() {
|
||||
const [platformName, setPlatformName] = useState<Platform>('')
|
||||
|
||||
useEffect(() => {
|
||||
async function getPlatform() {
|
||||
setPlatformName((window.electron.platform ?? '') as Platform)
|
||||
function getPlatform(): Platform {
|
||||
const platform = window.electron.platform ?? ''
|
||||
// https://nodejs.org/api/process.html#processplatform
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
return 'macos'
|
||||
case 'win32':
|
||||
return 'windows'
|
||||
// We don't currently care to distinguish between these.
|
||||
case 'android':
|
||||
case 'freebsd':
|
||||
case 'linux':
|
||||
case 'openbsd':
|
||||
case 'sunos':
|
||||
return 'linux'
|
||||
default:
|
||||
console.error('Unknown platform:', platform)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
if (isDesktop()) {
|
||||
void getPlatform()
|
||||
setPlatformName(getPlatform())
|
||||
} else {
|
||||
if (navigator.userAgent.indexOf('Mac') !== -1) {
|
||||
setPlatformName('macos')
|
||||
|
@ -2026,7 +2026,7 @@ describe('parsing errors', () => {
|
||||
expect(result).toBeInstanceOf(KCLError)
|
||||
const error = result as KCLError
|
||||
expect(error.kind).toBe('syntax')
|
||||
expect(error.msg).toBe('Unexpected token')
|
||||
expect(error.msg).toBe('Unexpected token: (')
|
||||
expect(error.sourceRanges).toEqual([[27, 28]])
|
||||
})
|
||||
})
|
||||
|
@ -563,6 +563,7 @@ export function createArrayExpression(
|
||||
start: 0,
|
||||
end: 0,
|
||||
digest: null,
|
||||
nonCodeMeta: { nonCodeNodes: {}, start: [], digest: null },
|
||||
elements,
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
@ -19,11 +19,6 @@ import init, {
|
||||
parse_project_route,
|
||||
base64_decode,
|
||||
} from '../wasm-lib/pkg/wasm_lib'
|
||||
import {
|
||||
configurationToSettingsPayload,
|
||||
projectConfigurationToSettingsPayload,
|
||||
} from 'lib/settings/settingsUtils'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { KCLError } from './errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
import { EngineCommandManager } from './std/engineConnection'
|
||||
@ -40,6 +35,9 @@ import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
import { TEST } from 'env'
|
||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||
import { err } from 'lib/trap'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { DeepPartial } from 'lib/types'
|
||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
|
||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
|
||||
@ -570,31 +568,30 @@ export function tomlStringify(toml: any): string | Error {
|
||||
return toml_stringify(JSON.stringify(toml))
|
||||
}
|
||||
|
||||
export function defaultAppSettings(): Partial<SaveSettingsPayload> {
|
||||
// Immediately go from Configuration -> Partial<SaveSettingsPayload>
|
||||
// The returned Rust type is Configuration but it's a lie. Every
|
||||
// property in that returned object is optional. The Partial<T> essentially
|
||||
// brings that type in-line with that definition.
|
||||
return configurationToSettingsPayload(default_app_settings())
|
||||
export function defaultAppSettings(): DeepPartial<Configuration> | Error {
|
||||
return default_app_settings()
|
||||
}
|
||||
|
||||
export function parseAppSettings(toml: string): Partial<SaveSettingsPayload> {
|
||||
const parsed = parse_app_settings(toml)
|
||||
return configurationToSettingsPayload(parsed)
|
||||
export function parseAppSettings(
|
||||
toml: string
|
||||
): DeepPartial<Configuration> | Error {
|
||||
return parse_app_settings(toml)
|
||||
}
|
||||
|
||||
export function defaultProjectSettings(): Partial<SaveSettingsPayload> {
|
||||
return projectConfigurationToSettingsPayload(default_project_settings())
|
||||
export function defaultProjectSettings():
|
||||
| DeepPartial<ProjectConfiguration>
|
||||
| Error {
|
||||
return default_project_settings()
|
||||
}
|
||||
|
||||
export function parseProjectSettings(
|
||||
toml: string
|
||||
): Partial<SaveSettingsPayload> {
|
||||
return projectConfigurationToSettingsPayload(parse_project_settings(toml))
|
||||
): DeepPartial<ProjectConfiguration> | Error {
|
||||
return parse_project_settings(toml)
|
||||
}
|
||||
|
||||
export function parseProjectRoute(
|
||||
configuration: Partial<SaveSettingsPayload>,
|
||||
configuration: DeepPartial<Configuration>,
|
||||
route_str: string
|
||||
): ProjectRoute | Error {
|
||||
return parse_project_route(JSON.stringify(configuration), route_str)
|
||||
|
@ -154,10 +154,6 @@ export function buildCommandArgument<
|
||||
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
||||
|
||||
if (arg.inputType === 'options') {
|
||||
if (!(arg.options || arg.optionsFromContext)) {
|
||||
throw new Error('Options must be provided for options input type')
|
||||
}
|
||||
|
||||
return {
|
||||
inputType: arg.inputType,
|
||||
...baseCommandArgument,
|
||||
|
@ -3,11 +3,9 @@ import { Models } from '@kittycad/lib'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
|
||||
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
|
||||
import {
|
||||
defaultAppSettings,
|
||||
tomlStringify,
|
||||
parseAppSettings,
|
||||
parseProjectSettings,
|
||||
} from 'lang/wasm'
|
||||
@ -18,6 +16,9 @@ import {
|
||||
PROJECT_SETTINGS_FILE_NAME,
|
||||
SETTINGS_FILE_NAME,
|
||||
} from './constants'
|
||||
import { DeepPartial } from './types'
|
||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
export { parseProjectRoute } from 'lang/wasm'
|
||||
|
||||
export async function renameProjectDirectory(
|
||||
@ -61,10 +62,13 @@ export async function renameProjectDirectory(
|
||||
}
|
||||
|
||||
export async function ensureProjectDirectoryExists(
|
||||
config: Partial<SaveSettingsPayload>
|
||||
config: DeepPartial<Configuration>
|
||||
): Promise<string | undefined> {
|
||||
const projectDir = config.app?.projectDirectory
|
||||
const projectDir =
|
||||
config.settings?.app?.project_directory ||
|
||||
config.settings?.project?.directory
|
||||
if (!projectDir) {
|
||||
console.error('projectDir is falsey', config)
|
||||
return Promise.reject(new Error('projectDir is falsey'))
|
||||
}
|
||||
try {
|
||||
@ -81,12 +85,13 @@ export async function ensureProjectDirectoryExists(
|
||||
export async function createNewProjectDirectory(
|
||||
projectName: string,
|
||||
initialCode?: string,
|
||||
configuration?: Partial<SaveSettingsPayload>
|
||||
configuration?: DeepPartial<Configuration> | Error
|
||||
): Promise<Project> {
|
||||
if (!configuration) {
|
||||
configuration = await readAppSettingsFile()
|
||||
}
|
||||
|
||||
if (err(configuration)) return Promise.reject(configuration)
|
||||
const mainDir = await ensureProjectDirectoryExists(configuration)
|
||||
|
||||
if (!projectName) {
|
||||
@ -124,11 +129,13 @@ export async function createNewProjectDirectory(
|
||||
}
|
||||
|
||||
export async function listProjects(
|
||||
configuration?: Partial<SaveSettingsPayload>
|
||||
configuration?: DeepPartial<Configuration> | Error
|
||||
): Promise<Project[]> {
|
||||
if (configuration === undefined) {
|
||||
configuration = await readAppSettingsFile()
|
||||
}
|
||||
|
||||
if (err(configuration)) return Promise.reject(configuration)
|
||||
const projectDir = await ensureProjectDirectoryExists(configuration)
|
||||
const projects = []
|
||||
if (!projectDir) return Promise.reject(new Error('projectDir was falsey'))
|
||||
@ -179,7 +186,7 @@ const collectAllFilesRecursiveFrom = async (path: string) => {
|
||||
return Promise.reject(new Error(`Path ${path} is not a directory`))
|
||||
}
|
||||
|
||||
const pathParts = path.split('/')
|
||||
const pathParts = path.split(window.electron.path.sep)
|
||||
let entry: FileEntry = {
|
||||
name: pathParts.slice(-1)[0],
|
||||
path,
|
||||
@ -358,10 +365,9 @@ export async function getProjectInfo(projectPath: string): Promise<Project> {
|
||||
// Write project settings file.
|
||||
export async function writeProjectSettingsFile(
|
||||
projectPath: string,
|
||||
configuration: Partial<SaveSettingsPayload>
|
||||
tomlStr: string
|
||||
): Promise<void> {
|
||||
const projectSettingsFilePath = await getProjectSettingsFilePath(projectPath)
|
||||
const tomlStr = tomlStringify({ settings: configuration })
|
||||
if (err(tomlStr)) return Promise.reject(tomlStr)
|
||||
return window.electron.writeFile(projectSettingsFilePath, tomlStr)
|
||||
}
|
||||
@ -407,13 +413,16 @@ const getProjectSettingsFilePath = async (projectPath: string) => {
|
||||
}
|
||||
|
||||
export const getInitialDefaultDir = async () => {
|
||||
if (!window.electron) {
|
||||
return ''
|
||||
}
|
||||
const dir = await window.electron.getPath('documents')
|
||||
return window.electron.path.join(dir, PROJECT_FOLDER)
|
||||
}
|
||||
|
||||
export const readProjectSettingsFile = async (
|
||||
projectPath: string
|
||||
): Promise<Partial<SaveSettingsPayload>> => {
|
||||
): Promise<DeepPartial<ProjectConfiguration>> => {
|
||||
let settingsPath = await getProjectSettingsFilePath(projectPath)
|
||||
|
||||
// Check if this file exists.
|
||||
@ -428,6 +437,9 @@ export const readProjectSettingsFile = async (
|
||||
|
||||
const configToml = await window.electron.readFile(settingsPath)
|
||||
const configObj = parseProjectSettings(configToml)
|
||||
if (err(configObj)) {
|
||||
return Promise.reject(configObj)
|
||||
}
|
||||
return configObj
|
||||
}
|
||||
|
||||
@ -438,23 +450,25 @@ export const readAppSettingsFile = async () => {
|
||||
} catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
const config = defaultAppSettings()
|
||||
if (!config.app) {
|
||||
if (err(config)) return Promise.reject(config)
|
||||
if (!config.settings?.app)
|
||||
return Promise.reject(new Error('config.app is falsey'))
|
||||
}
|
||||
config.app.projectDirectory = await getInitialDefaultDir()
|
||||
|
||||
config.settings.app.project_directory = await getInitialDefaultDir()
|
||||
return config
|
||||
}
|
||||
}
|
||||
const configToml = await window.electron.readFile(settingsPath)
|
||||
const configObj = parseAppSettings(configToml)
|
||||
if (err(configObj)) {
|
||||
return Promise.reject(configObj)
|
||||
}
|
||||
|
||||
return configObj
|
||||
}
|
||||
|
||||
export const writeAppSettingsFile = async (
|
||||
config: Partial<SaveSettingsPayload>
|
||||
) => {
|
||||
export const writeAppSettingsFile = async (tomlStr: string) => {
|
||||
const appSettingsFilePath = await getAppSettingsFilePath()
|
||||
const tomlStr = tomlStringify({ settings: config })
|
||||
if (err(tomlStr)) return Promise.reject(tomlStr)
|
||||
return window.electron.writeFile(appSettingsFilePath, tomlStr)
|
||||
}
|
||||
|
@ -38,6 +38,9 @@ const bracket = startSketchOn('XY')
|
||||
tags: [getPreviousAdjacentEdge(outerEdge)]
|
||||
}, %)`
|
||||
|
||||
/**
|
||||
* @throws Error if the search text is not found in the example code.
|
||||
*/
|
||||
function findLineInExampleCode({
|
||||
searchText,
|
||||
example = bracket,
|
||||
@ -48,6 +51,8 @@ function findLineInExampleCode({
|
||||
const lines = example.split('\n')
|
||||
const lineNumber = lines.findIndex((l) => l.includes(searchText)) + 1
|
||||
if (lineNumber === 0) {
|
||||
// We are exporting a constant, so we don't want to return an Error.
|
||||
// eslint-disable-next-line suggest-no-throw/suggest-no-throw
|
||||
throw new Error(
|
||||
`Could not find the line with search text "${searchText}" in the example code. Was it removed?`
|
||||
)
|
||||
|
@ -65,6 +65,13 @@ export async function exportMake(data: ArrayBuffer): Promise<Response | null> {
|
||||
|
||||
console.log('response', response)
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Error exporting', response)
|
||||
const text = await response.text()
|
||||
toast.error('Error exporting: ' + response.statusText + ' ' + text)
|
||||
return null
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Error exporting', error)
|
||||
|
20
src/lib/machine-api.d.ts
vendored
@ -271,7 +271,7 @@ export interface components {
|
||||
* Format: int64
|
||||
* @description The error number.
|
||||
*/
|
||||
errorno: number
|
||||
errorno?: number | null
|
||||
/** @description The reason for the message. */
|
||||
reason?: components['schemas']['Reason'] | null
|
||||
/** @description The result of the command. */
|
||||
@ -282,7 +282,7 @@ export interface components {
|
||||
* Format: int64
|
||||
* @description The target temperature.
|
||||
*/
|
||||
tar_temp: number
|
||||
tar_temp?: number | null
|
||||
/**
|
||||
* Format: int64
|
||||
* @description The target.
|
||||
@ -523,6 +523,22 @@ export interface components {
|
||||
} & {
|
||||
[key: string]: unknown
|
||||
})
|
||||
| ({
|
||||
/** @enum {string} */
|
||||
command: 'gcode_file'
|
||||
/** @description The param. */
|
||||
param?: string | null
|
||||
/** @description The print type. */
|
||||
print_type?: string | null
|
||||
/** @description The reason for the message. */
|
||||
reason: components['schemas']['Reason']
|
||||
/** @description The result of the command. */
|
||||
result: components['schemas']['Result']
|
||||
/** @description The sequence id. */
|
||||
sequence_id: components['schemas']['SequenceId']
|
||||
} & {
|
||||
[key: string]: unknown
|
||||
})
|
||||
| ({
|
||||
/** @enum {string} */
|
||||
command: 'project_file'
|
||||
|
@ -51,6 +51,19 @@ export class MachineManager {
|
||||
return this._machineApiIp
|
||||
}
|
||||
|
||||
// Get the reason message for why there are no machines.
|
||||
noMachinesReason(): string | undefined {
|
||||
if (this.machineCount() > 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (this.machineApiIp === null) {
|
||||
return 'Machine API server was not discovered'
|
||||
}
|
||||
|
||||
return 'Machine API server was discovered, but no machines are available'
|
||||
}
|
||||
|
||||
get currentMachine(): components['schemas']['Machine'] | null {
|
||||
return this._currentMachine
|
||||
}
|
||||
|
@ -4,9 +4,10 @@ import { isDesktop } from './isDesktop'
|
||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||
import { parseProjectRoute, readAppSettingsFile } from './desktop'
|
||||
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
|
||||
import { SaveSettingsPayload } from './settings/settingsTypes'
|
||||
import { err } from 'lib/trap'
|
||||
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
|
||||
import { DeepPartial } from './types'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
|
||||
const prependRoutes =
|
||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||
@ -39,7 +40,7 @@ export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${
|
||||
|
||||
export async function getProjectMetaByRouteId(
|
||||
id?: string,
|
||||
configuration?: Partial<SaveSettingsPayload> | Error
|
||||
configuration?: DeepPartial<Configuration> | Error
|
||||
): Promise<ProjectRoute | undefined> {
|
||||
if (!id) return undefined
|
||||
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from 'lib/constants'
|
||||
import { loadAndValidateSettings } from './settings/settingsUtils'
|
||||
import makeUrlPathRelative from './makeUrlPathRelative'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
import { codeManager } from 'lib/singletons'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
import {
|
||||
getProjectInfo,
|
||||
@ -107,8 +107,6 @@ export const fileLoader: LoaderFunction = async (
|
||||
// the file system and not the editor.
|
||||
codeManager.updateCurrentFilePath(current_file_path)
|
||||
codeManager.updateCodeStateEditor(code)
|
||||
// We don't want to call await on execute code since we don't want to block the UI
|
||||
kclManager.executeCode(true)
|
||||
}
|
||||
|
||||
// Set the file system manager to the project path
|
||||
@ -125,14 +123,22 @@ export const fileLoader: LoaderFunction = async (
|
||||
default_file: project_path,
|
||||
}
|
||||
|
||||
const maybeProjectInfo = isDesktop()
|
||||
? await getProjectInfo(project_path)
|
||||
: null
|
||||
|
||||
console.log('maybeProjectInfo', {
|
||||
maybeProjectInfo,
|
||||
defaultProjectData,
|
||||
projectPathData,
|
||||
})
|
||||
|
||||
const projectData: IndexLoaderData = {
|
||||
code,
|
||||
project: isDesktop()
|
||||
? (await getProjectInfo(project_path)) ?? defaultProjectData
|
||||
: defaultProjectData,
|
||||
project: maybeProjectInfo ?? defaultProjectData,
|
||||
file: {
|
||||
name: current_file_name || '',
|
||||
path: current_file_path?.split('/').slice(0, -1).join('/') ?? '',
|
||||
path: current_file_path || '',
|
||||
children: [],
|
||||
},
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
} from 'lib/desktop'
|
||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
import { DeepPartial } from 'lib/types'
|
||||
|
||||
/**
|
||||
* Convert from a rust settings struct into the JS settings struct.
|
||||
@ -28,8 +29,8 @@ import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
* for hiding and showing settings.
|
||||
**/
|
||||
export function configurationToSettingsPayload(
|
||||
configuration: Configuration
|
||||
): Partial<SaveSettingsPayload> {
|
||||
configuration: DeepPartial<Configuration>
|
||||
): DeepPartial<SaveSettingsPayload> {
|
||||
return {
|
||||
app: {
|
||||
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
|
||||
@ -66,8 +67,8 @@ export function configurationToSettingsPayload(
|
||||
}
|
||||
|
||||
export function projectConfigurationToSettingsPayload(
|
||||
configuration: ProjectConfiguration
|
||||
): Partial<SaveSettingsPayload> {
|
||||
configuration: DeepPartial<ProjectConfiguration>
|
||||
): DeepPartial<SaveSettingsPayload> {
|
||||
return {
|
||||
app: {
|
||||
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
|
||||
@ -106,7 +107,7 @@ function localStorageProjectSettingsPath() {
|
||||
}
|
||||
|
||||
export function readLocalStorageAppSettingsFile():
|
||||
| Partial<SaveSettingsPayload>
|
||||
| DeepPartial<Configuration>
|
||||
| Error {
|
||||
// TODO: Remove backwards compatibility after a few releases.
|
||||
let stored =
|
||||
@ -132,7 +133,7 @@ export function readLocalStorageAppSettingsFile():
|
||||
}
|
||||
|
||||
function readLocalStorageProjectSettingsFile():
|
||||
| Partial<SaveSettingsPayload>
|
||||
| DeepPartial<ProjectConfiguration>
|
||||
| Error {
|
||||
// TODO: Remove backwards compatibility after a few releases.
|
||||
let stored = localStorage.getItem(localStorageProjectSettingsPath()) ?? ''
|
||||
@ -156,7 +157,7 @@ function readLocalStorageProjectSettingsFile():
|
||||
|
||||
export interface AppSettings {
|
||||
settings: ReturnType<typeof createSettings>
|
||||
configuration: Partial<SaveSettingsPayload>
|
||||
configuration: DeepPartial<Configuration>
|
||||
}
|
||||
|
||||
export async function loadAndValidateSettings(
|
||||
@ -175,7 +176,11 @@ export async function loadAndValidateSettings(
|
||||
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
|
||||
|
||||
const settings = createSettings()
|
||||
setSettingsAtLevel(settings, 'user', appSettingsPayload)
|
||||
setSettingsAtLevel(
|
||||
settings,
|
||||
'user',
|
||||
configurationToSettingsPayload(appSettingsPayload)
|
||||
)
|
||||
|
||||
// Load the project settings if they exist
|
||||
if (projectPath) {
|
||||
@ -187,11 +192,18 @@ export async function loadAndValidateSettings(
|
||||
return Promise.reject(new Error('Invalid project settings'))
|
||||
|
||||
const projectSettingsPayload = projectSettings
|
||||
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
|
||||
setSettingsAtLevel(
|
||||
settings,
|
||||
'project',
|
||||
projectConfigurationToSettingsPayload(projectSettingsPayload)
|
||||
)
|
||||
}
|
||||
|
||||
// Return the settings object
|
||||
return { settings, configuration: appSettingsPayload }
|
||||
return {
|
||||
settings,
|
||||
configuration: appSettingsPayload,
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSettings(
|
||||
@ -204,21 +216,14 @@ export async function saveSettings(
|
||||
|
||||
// Get the user settings.
|
||||
const jsAppSettings = getChangedSettingsAtLevel(allSettings, 'user')
|
||||
const tomlString = tomlStringify({ settings: jsAppSettings })
|
||||
if (err(tomlString)) return
|
||||
|
||||
// Parse this as a Configuration.
|
||||
const appSettings = parseAppSettings(tomlString)
|
||||
if (err(appSettings)) return
|
||||
|
||||
const tomlString2 = tomlStringify({ settings: appSettings })
|
||||
if (err(tomlString2)) return
|
||||
const appTomlString = tomlStringify({ settings: jsAppSettings })
|
||||
if (err(appTomlString)) return
|
||||
|
||||
// Write the app settings.
|
||||
if (onDesktop) {
|
||||
await writeAppSettingsFile(appSettings)
|
||||
await writeAppSettingsFile(appTomlString)
|
||||
} else {
|
||||
localStorage.setItem(localStorageAppSettingsPath(), tomlString2)
|
||||
localStorage.setItem(localStorageAppSettingsPath(), appTomlString)
|
||||
}
|
||||
|
||||
if (!projectPath) {
|
||||
@ -231,19 +236,11 @@ export async function saveSettings(
|
||||
const projectTomlString = tomlStringify({ settings: jsProjectSettings })
|
||||
if (err(projectTomlString)) return
|
||||
|
||||
// Parse this as a Configuration.
|
||||
const projectSettings = parseProjectSettings(projectTomlString)
|
||||
if (err(projectSettings)) return
|
||||
|
||||
const tomlStr = tomlStringify(projectSettings)
|
||||
|
||||
if (err(tomlStr)) return
|
||||
|
||||
// Write the project settings.
|
||||
if (onDesktop) {
|
||||
await writeProjectSettingsFile(projectPath, projectSettings)
|
||||
await writeProjectSettingsFile(projectPath, projectTomlString)
|
||||
} else {
|
||||
localStorage.setItem(localStorageProjectSettingsPath(), tomlStr)
|
||||
localStorage.setItem(localStorageProjectSettingsPath(), projectTomlString)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,3 +95,9 @@ export function isEnumMember<T extends Record<string, unknown>>(
|
||||
) {
|
||||
return Object.values(e).includes(v)
|
||||
}
|
||||
|
||||
// utility type to make all *nested* object properties optional
|
||||
// https://www.geodev.me/blog/deeppartial-in-typescript
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
|
||||
}
|
||||
|
19
src/main.ts
@ -131,8 +131,6 @@ ipcMain.handle('kittycad', (event, data) => {
|
||||
)(data.args)
|
||||
})
|
||||
|
||||
const SERVICE_NAME = '_machine-api._tcp.local.'
|
||||
|
||||
ipcMain.handle('find_machine_api', () => {
|
||||
const timeoutAfterMs = 5000
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -144,8 +142,19 @@ ipcMain.handle('find_machine_api', () => {
|
||||
resolve(null)
|
||||
})
|
||||
console.log('Looking for machine API...')
|
||||
bonjourEt.find({ type: SERVICE_NAME }, (service: Service) => {
|
||||
resolve(service.fqdn)
|
||||
})
|
||||
bonjourEt.find(
|
||||
{ protocol: 'tcp', type: 'machine-api' },
|
||||
(service: Service) => {
|
||||
console.log('Found machine API!', JSON.stringify(service))
|
||||
if (!service.addresses || service.addresses?.length === 0) {
|
||||
console.log('No addresses found for machine API!')
|
||||
return resolve(null)
|
||||
}
|
||||
const ip = service.addresses[0]
|
||||
const port = service.port
|
||||
// We want to return the ip address of the machine API.
|
||||
resolve(`${ip}:${port}`)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -145,7 +145,7 @@ function OnboardingIntroductionInner() {
|
||||
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||
<h1 className="flex flex-wrap items-center gap-4 text-3xl font-bold">
|
||||
<img
|
||||
src={`./zma-logomark${getLogoTheme()}.svg`}
|
||||
src={`${isDesktop() ? '.' : ''}/zma-logomark${getLogoTheme()}.svg`}
|
||||
alt={APP_NAME}
|
||||
className="h-20 max-w-full"
|
||||
/>
|
||||
|
@ -13,11 +13,18 @@ import { AllSettingsFields } from 'components/Settings/AllSettingsFields'
|
||||
import { AllKeybindingsFields } from 'components/Settings/AllKeybindingsFields'
|
||||
import { KeybindingsSectionsList } from 'components/Settings/KeybindingsSectionsList'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
|
||||
import { NODE_ENV } from 'env'
|
||||
|
||||
export const APP_VERSION = isDesktop()
|
||||
? // @ts-ignore
|
||||
window.electron.packageJson.version
|
||||
: 'main'
|
||||
const isTestEnv = window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true'
|
||||
|
||||
export const APP_VERSION =
|
||||
isTestEnv && NODE_ENV === 'development'
|
||||
? '11.22.33'
|
||||
: isDesktop()
|
||||
? // @ts-ignore
|
||||
window.electron.packageJson.version
|
||||
: 'main'
|
||||
|
||||
export const Settings = () => {
|
||||
const navigate = useNavigate()
|
||||
|
82
src/wasm-lib/Cargo.lock
generated
@ -169,7 +169,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -180,7 +180,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -191,7 +191,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -443,7 +443,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -643,7 +643,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -654,7 +654,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -709,7 +709,7 @@ checksum = "4078275de501a61ceb9e759d37bdd3d7210e654dbc167ac1a3678ef4435ed57b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@ -724,7 +724,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive-docs"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"anyhow",
|
||||
@ -738,7 +738,7 @@ dependencies = [
|
||||
"rustfmt-wrapper",
|
||||
"serde",
|
||||
"serde_tokenstream",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -749,7 +749,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -776,7 +776,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -948,7 +948,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1038,7 +1038,7 @@ dependencies = [
|
||||
"inflections",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1397,7 +1397,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
@ -1462,12 +1462,12 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hyper",
|
||||
@ -1841,7 +1841,7 @@ dependencies = [
|
||||
"regex",
|
||||
"regex-syntax 0.8.3",
|
||||
"structmeta",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1894,7 +1894,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2058,7 +2058,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2071,7 +2071,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-build-config",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2533,7 +2533,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2607,7 +2607,7 @@ checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2618,7 +2618,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2642,7 +2642,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2663,7 +2663,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2800,7 +2800,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"structmeta-derive",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2811,7 +2811,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2855,9 +2855,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.74"
|
||||
version = "2.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
|
||||
checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2878,7 +2878,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2985,7 +2985,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3056,9 +3056,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.39.2"
|
||||
version = "1.39.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
|
||||
checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@ -3080,7 +3080,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3233,7 +3233,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3261,7 +3261,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3338,7 +3338,7 @@ checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
@ -3502,7 +3502,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3563,7 +3563,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -3598,7 +3598,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -3924,7 +3924,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
"syn 2.0.75",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -17,7 +17,7 @@ gloo-utils = "0.2.0"
|
||||
kcl-lib = { path = "kcl" }
|
||||
kittycad.workspace = true
|
||||
serde_json = "1.0.125"
|
||||
tokio = { version = "1.39.2", features = ["sync"] }
|
||||
tokio = { version = "1.39.3", features = ["sync"] }
|
||||
toml = "0.8.19"
|
||||
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
|
||||
wasm-bindgen = "0.2.91"
|
||||
@ -30,7 +30,7 @@ image = { version = "0.25.1", default-features = false, features = ["png"] }
|
||||
kittycad = { workspace = true, default-features = true }
|
||||
pretty_assertions = "1.4.0"
|
||||
reqwest = { version = "0.11.26", default-features = false }
|
||||
tokio = { version = "1.39.2", features = ["rt-multi-thread", "macros", "time"] }
|
||||
tokio = { version = "1.39.3", features = ["rt-multi-thread", "macros", "time"] }
|
||||
twenty-twenty = "0.8"
|
||||
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "derive-docs"
|
||||
description = "A tool for generating documentation from Rust derive macros"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
@ -20,7 +20,7 @@ quote = "1"
|
||||
regex = "1.10"
|
||||
serde = { version = "1.0.208", features = ["derive"] }
|
||||
serde_tokenstream = "0.2"
|
||||
syn = { version = "2.0.74", features = ["full"] }
|
||||
syn = { version = "2.0.75", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.86"
|
||||
|
@ -15,7 +15,7 @@ databake = "0.1.8"
|
||||
kcl-lib = { path = "../kcl" }
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "2.0.74", features = ["full"] }
|
||||
syn = { version = "2.0.75", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.0"
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-test-server"
|
||||
description = "A test server for KCL"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
@ -12,4 +12,4 @@ kcl-lib = { version = "0.2", path = "../kcl" }
|
||||
pico-args = "0.5.0"
|
||||
serde = { version = "1.0.208", features = ["derive"] }
|
||||
serde_json = "1.0.125"
|
||||
tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread"] }
|
||||
tokio = { version = "1.39.3", features = ["macros", "rt-multi-thread"] }
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
@ -20,7 +20,7 @@ clap = { version = "4.5.16", default-features = false, optional = true }
|
||||
convert_case = "0.6.0"
|
||||
dashmap = "6.0.1"
|
||||
databake = { version = "0.1.8", features = ["derive"] }
|
||||
derive-docs = { version = "0.1.23", path = "../derive-docs" }
|
||||
derive-docs = { version = "0.1.24", path = "../derive-docs" }
|
||||
form_urlencoded = "1.2.1"
|
||||
futures = { version = "0.3.30" }
|
||||
git_rev = "0.1.0"
|
||||
@ -50,7 +50,7 @@ zip = { version = "2.0.0", default-features = false }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = { version = "0.3.69" }
|
||||
tokio = { version = "1.39.2", features = ["sync", "time"] }
|
||||
tokio = { version = "1.39.3", features = ["sync", "time"] }
|
||||
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
|
||||
wasm-bindgen = "0.2.91"
|
||||
wasm-bindgen-futures = "0.4.42"
|
||||
@ -59,7 +59,7 @@ web-sys = { version = "0.3.69", features = ["console"] }
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
approx = "0.5"
|
||||
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
|
||||
tokio = { version = "1.39.2", features = ["full"] }
|
||||
tokio = { version = "1.39.3", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.23.1", features = ["rustls-tls-native-roots"] }
|
||||
tower-lsp = { version = "0.20.0", features = ["proposed"] }
|
||||
|
||||
|
@ -1149,6 +1149,15 @@ pub enum NonCodeValue {
|
||||
NewLine,
|
||||
}
|
||||
|
||||
impl NonCodeValue {
|
||||
fn should_cause_array_newline(&self) -> bool {
|
||||
match self {
|
||||
Self::InlineComment { .. } => false,
|
||||
Self::Shebang { .. } | Self::BlockComment { .. } | Self::NewLineBlockComment { .. } | Self::NewLine => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
|
||||
#[databake(path = kcl_lib::ast::types)]
|
||||
#[ts(export)]
|
||||
@ -1160,6 +1169,18 @@ pub struct NonCodeMeta {
|
||||
pub digest: Option<Digest>,
|
||||
}
|
||||
|
||||
impl NonCodeMeta {
|
||||
/// Does this contain anything?
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.non_code_nodes.is_empty() && self.start.is_empty()
|
||||
}
|
||||
|
||||
/// How many non-code values does this have?
|
||||
pub fn non_code_nodes_len(&self) -> usize {
|
||||
self.non_code_nodes.values().map(|x| x.len()).sum()
|
||||
}
|
||||
}
|
||||
|
||||
// implement Deserialize manually because we to force the keys of non_code_nodes to be usize
|
||||
// and by default the ts type { [statementIndex: number]: NonCodeNode } serializes to a string i.e. "0", "1", etc.
|
||||
impl<'de> Deserialize<'de> for NonCodeMeta {
|
||||
@ -2224,11 +2245,13 @@ impl From<PipeSubstitution> for Expr {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
|
||||
#[databake(path = kcl_lib::ast::types)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub struct ArrayExpression {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub elements: Vec<Expr>,
|
||||
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
|
||||
pub non_code_meta: NonCodeMeta,
|
||||
|
||||
pub digest: Option<Digest>,
|
||||
}
|
||||
@ -2247,6 +2270,7 @@ impl ArrayExpression {
|
||||
start: 0,
|
||||
end: 0,
|
||||
elements,
|
||||
non_code_meta: Default::default(),
|
||||
digest: None,
|
||||
}
|
||||
}
|
||||
@ -2280,38 +2304,70 @@ impl ArrayExpression {
|
||||
}
|
||||
|
||||
fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String {
|
||||
let flat_recast = format!(
|
||||
"[{}]",
|
||||
self.elements
|
||||
.iter()
|
||||
.map(|el| el.recast(options, 0, false))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
);
|
||||
let max_array_length = 40;
|
||||
if flat_recast.len() > max_array_length {
|
||||
let inner_indentation = if is_in_pipe {
|
||||
options.get_indentation_offset_pipe(indentation_level + 1)
|
||||
} else {
|
||||
options.get_indentation(indentation_level + 1)
|
||||
};
|
||||
format!(
|
||||
"[\n{}{}\n{}]",
|
||||
inner_indentation,
|
||||
self.elements
|
||||
.iter()
|
||||
.map(|el| el.recast(options, indentation_level, is_in_pipe))
|
||||
.collect::<Vec<String>>()
|
||||
.join(format!(",\n{}", inner_indentation).as_str()),
|
||||
if is_in_pipe {
|
||||
options.get_indentation_offset_pipe(indentation_level)
|
||||
// Reconstruct the order of items in the array.
|
||||
// An item can be an element (i.e. an expression for a KCL value),
|
||||
// or a non-code item (e.g. a comment)
|
||||
let num_items = self.elements.len() + self.non_code_meta.non_code_nodes_len();
|
||||
let mut elems = self.elements.iter();
|
||||
let mut found_line_comment = false;
|
||||
let mut format_items: Vec<_> = (0..num_items)
|
||||
.flat_map(|i| {
|
||||
if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
|
||||
noncode
|
||||
.iter()
|
||||
.map(|nc| {
|
||||
found_line_comment |= nc.value.should_cause_array_newline();
|
||||
nc.format("")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
options.get_indentation(indentation_level)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
flat_recast
|
||||
let el = elems.next().unwrap();
|
||||
let s = format!("{}, ", el.recast(options, 0, false));
|
||||
vec![s]
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Format these items into a one-line array.
|
||||
if let Some(item) = format_items.last_mut() {
|
||||
if let Some(norm) = item.strip_suffix(", ") {
|
||||
*item = norm.to_owned();
|
||||
}
|
||||
}
|
||||
let format_items = format_items; // Remove mutability
|
||||
let flat_recast = format!("[{}]", format_items.join(""));
|
||||
|
||||
// We might keep the one-line representation, if it's short enough.
|
||||
let max_array_length = 40;
|
||||
let multi_line = flat_recast.len() > max_array_length || found_line_comment;
|
||||
if !multi_line {
|
||||
return flat_recast;
|
||||
}
|
||||
|
||||
// Otherwise, we format a multi-line representation.
|
||||
let inner_indentation = if is_in_pipe {
|
||||
options.get_indentation_offset_pipe(indentation_level + 1)
|
||||
} else {
|
||||
options.get_indentation(indentation_level + 1)
|
||||
};
|
||||
let formatted_array_lines = format_items
|
||||
.iter()
|
||||
.map(|s| {
|
||||
format!(
|
||||
"{inner_indentation}{}{}",
|
||||
if let Some(x) = s.strip_suffix(" ") { x } else { s },
|
||||
if s.ends_with('\n') { "" } else { "\n" }
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("")
|
||||
.to_owned();
|
||||
let end_indent = if is_in_pipe {
|
||||
options.get_indentation_offset_pipe(indentation_level)
|
||||
} else {
|
||||
options.get_indentation(indentation_level)
|
||||
};
|
||||
format!("[\n{formatted_array_lines}{end_indent}]")
|
||||
}
|
||||
|
||||
/// Returns a hover value that includes the given character position.
|
||||
@ -5838,6 +5894,103 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recast_array_with_comments() {
|
||||
use winnow::Parser;
|
||||
for (i, (input, expected, reason)) in [
|
||||
(
|
||||
"\
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
]",
|
||||
"\
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20
|
||||
]",
|
||||
"preserves multi-line arrays",
|
||||
),
|
||||
(
|
||||
"\
|
||||
[
|
||||
1,
|
||||
// 2,
|
||||
3
|
||||
]",
|
||||
"\
|
||||
[
|
||||
1,
|
||||
// 2,
|
||||
3
|
||||
]",
|
||||
"preserves comments",
|
||||
),
|
||||
(
|
||||
"\
|
||||
[
|
||||
1,
|
||||
2,
|
||||
// 3
|
||||
]",
|
||||
"\
|
||||
[
|
||||
1,
|
||||
2,
|
||||
// 3
|
||||
]",
|
||||
"preserves comments at the end of the array",
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let tokens = crate::token::lexer(input).unwrap();
|
||||
let expr = crate::parser::parser_impl::array_elem_by_elem.parse(&tokens).unwrap();
|
||||
assert_eq!(
|
||||
expr.recast(&FormatOptions::new(), 0, false),
|
||||
expected,
|
||||
"failed test {i}, which is testing that recasting {reason}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_params() {
|
||||
for (i, (test_name, expected, function_expr)) in [
|
||||
@ -6039,7 +6192,7 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([57, 59])], message: "Unexpected token" }"#
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([57, 59])], message: "Unexpected token: |>" }"#
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2898,7 +2898,7 @@ let notTagIdentifier = !myTag";
|
||||
// a runtime error instead.
|
||||
parse_execute(code10).await.unwrap_err().downcast::<KclError>().unwrap(),
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
message: "Unexpected token".to_owned(),
|
||||
message: "Unexpected token: !".to_owned(),
|
||||
source_ranges: vec![SourceRange([14, 15])],
|
||||
})
|
||||
);
|
||||
@ -2911,7 +2911,7 @@ let notPipeSub = 1 |> identity(!%))";
|
||||
// a runtime error instead.
|
||||
parse_execute(code11).await.unwrap_err().downcast::<KclError>().unwrap(),
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
message: "Unexpected token".to_owned(),
|
||||
message: "Unexpected token: |>".to_owned(),
|
||||
source_ranges: vec![SourceRange([54, 56])],
|
||||
})
|
||||
);
|
||||
|