Merge branch 'main' into pierremtb/issue3528-Add-electron-updater

This commit is contained in:
Pierre Jacquier
2024-08-23 05:10:32 -04:00
committed by GitHub
19 changed files with 283 additions and 156 deletions

View File

@ -124,7 +124,7 @@ const extrude001 = extrude(-10, sketch001)`
await expect(cmdSearchBar).not.toBeVisible() await expect(cmdSearchBar).not.toBeVisible()
// Now try the same, but with the keyboard shortcut, check focus // Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K') await page.keyboard.press('ControlOrMeta+K')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused() await expect(cmdSearchBar).toBeFocused()
@ -185,7 +185,7 @@ const extrude001 = extrude(-10, sketch001)`
await page.locator('.cm-content').click() await page.locator('.cm-content').click()
// Now try the same, but with the keyboard shortcut, check focus // Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('Meta+K') await page.keyboard.press('ControlOrMeta+K')
let cmdSearchBar = page.getByPlaceholder('Search commands') let cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
@ -250,7 +250,7 @@ const extrude001 = extrude(-10, sketch001)`
await page.getByRole('button', { name: 'Extrude' }).isEnabled() await page.getByRole('button', { name: 'Extrude' }).isEnabled()
let cmdSearchBar = page.getByPlaceholder('Search commands') let cmdSearchBar = page.getByPlaceholder('Search commands')
await page.keyboard.press('Meta+K') await page.keyboard.press('ControlOrMeta+K')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
// Search for extrude command and choose it // Search for extrude command and choose it

View File

@ -332,7 +332,6 @@ test.describe('Copilot ghost text', () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control'
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -349,10 +348,10 @@ test.describe('Copilot ghost text', () => {
) )
// Going elsewhere in the code should hide the ghost text. // Going elsewhere in the code should hide the ghost text.
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.keyboard.press('KeyZ') await page.keyboard.press('KeyZ')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
@ -368,8 +367,6 @@ test.describe('Copilot ghost text', () => {
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control'
await page.waitForTimeout(800) await page.waitForTimeout(800)
await u.codeLocator.click() await u.codeLocator.click()
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
@ -382,17 +379,17 @@ test.describe('Copilot ghost text', () => {
await page.waitForTimeout(800) await page.waitForTimeout(800)
// Ctrl+z // Ctrl+z
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ') await page.keyboard.press('KeyZ')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)
// Ctrl+shift+z // Ctrl+shift+z
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.keyboard.press('KeyZ') await page.keyboard.press('KeyZ')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await expect(page.locator('.cm-content')).toHaveText(`{thing: "blah"}`) await expect(page.locator('.cm-content')).toHaveText(`{thing: "blah"}`)
@ -411,14 +408,14 @@ test.describe('Copilot ghost text', () => {
) )
// Once for the enter. // Once for the enter.
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ') await page.keyboard.press('KeyZ')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
// Once for the text. // Once for the text.
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ') await page.keyboard.press('KeyZ')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()

View File

@ -16,7 +16,6 @@ test.describe('Editor tests', () => {
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control'
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
@ -29,9 +28,9 @@ test.describe('Editor tests', () => {
|> line([-20, 0], %) |> line([-20, 0], %)
|> close(%)`) |> close(%)`)
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('/') await page.keyboard.press('/')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XY') .toHaveText(`const sketch001 = startSketchOn('XY')
@ -42,9 +41,9 @@ test.describe('Editor tests', () => {
// |> close(%)`) // |> close(%)`)
// uncomment the code // uncomment the code
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('/') await page.keyboard.press('/')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const sketch001 = startSketchOn('XY') .toHaveText(`const sketch001 = startSketchOn('XY')
@ -148,9 +147,7 @@ test.describe('Editor tests', () => {
// Delete all the code. // Delete all the code.
await page.locator('.cm-content').click() await page.locator('.cm-content').click()
// Select all // Select all
await page.keyboard.press('Control+A') await page.keyboard.press('ControlOrMeta+A')
await page.keyboard.press('Backspace')
await page.keyboard.press('Meta+A')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
await expect(page.locator('.cm-content')).toHaveText(``) await expect(page.locator('.cm-content')).toHaveText(``)

View File

@ -1233,18 +1233,13 @@ test(
await page.getByText('mike_stress_test').click() 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 test.step('select all in code editor, check its length', async () => {
await u.codeLocator.click() await u.codeLocator.click()
// expect u.codeLocator to have some text // expect u.codeLocator to have some text
await expect(u.codeLocator).toContainText('line(') await expect(u.codeLocator).toContainText('line(')
await page.keyboard.down(modifier) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyA') await page.keyboard.press('KeyA')
await page.keyboard.up(modifier) await page.keyboard.up('ControlOrMeta')
// check the length of the selected text // check the length of the selected text
const selectedText = await page.evaluate(() => { const selectedText = await page.evaluate(() => {
@ -1260,9 +1255,9 @@ test(
await test.step('delete all the text, select again and verify there are no characters left', async () => { 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.press('Backspace')
await page.keyboard.down(modifier) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyA') await page.keyboard.press('KeyA')
await page.keyboard.up(modifier) await page.keyboard.up('ControlOrMeta')
// check the length of the selected text // check the length of the selected text
const selectedText = await page.evaluate(() => { const selectedText = await page.evaluate(() => {
@ -1329,7 +1324,7 @@ test.describe('Renaming in the file tree', () => {
process.platform === 'win32', process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' 'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
) )
const { electronApp, page } = await setupElectron({ const { electronApp, page, dir } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true }) await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
@ -1357,6 +1352,16 @@ test.describe('Renaming in the file tree', () => {
// Constants and locators // Constants and locators
const projectLink = page.getByText('Test Project') const projectLink = page.getByText('Test Project')
const projectMenuButton = page.getByTestId('project-sidebar-toggle') const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const checkUnRenamedFS = () => {
const filePath = join(dir, 'Test Project', 'fileToRename.kcl')
return fs.existsSync(filePath)
}
const newFileName = 'newFileName'
const checkRenamedFS = () => {
const filePath = join(dir, 'Test Project', `${newFileName}.kcl`)
return fs.existsSync(filePath)
}
const fileToRename = page const fileToRename = page
.getByRole('listitem') .getByRole('listitem')
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) }) .filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
@ -1365,7 +1370,6 @@ test.describe('Renaming in the file tree', () => {
.filter({ has: page.getByRole('button', { name: 'newFileName.kcl' }) }) .filter({ has: page.getByRole('button', { name: 'newFileName.kcl' }) })
const renameMenuItem = page.getByRole('button', { name: 'Rename' }) const renameMenuItem = page.getByRole('button', { name: 'Rename' })
const renameInput = page.getByPlaceholder('fileToRename.kcl') const renameInput = page.getByPlaceholder('fileToRename.kcl')
const newFileName = 'newFileName'
const codeLocator = page.locator('.cm-content') const codeLocator = page.locator('.cm-content')
await test.step('Open project and file pane', async () => { await test.step('Open project and file pane', async () => {
@ -1376,6 +1380,8 @@ test.describe('Renaming in the file tree', () => {
await u.openFilePanel() await u.openFilePanel()
await expect(fileToRename).toBeVisible() await expect(fileToRename).toBeVisible()
expect(checkUnRenamedFS()).toBeTruthy()
expect(checkRenamedFS()).toBeFalsy()
await fileToRename.click() await fileToRename.click()
await expect(projectMenuButton).toContainText('fileToRename.kcl') await expect(projectMenuButton).toContainText('fileToRename.kcl')
await u.openKclCodePanel() await u.openKclCodePanel()
@ -1394,6 +1400,8 @@ test.describe('Renaming in the file tree', () => {
await test.step('Verify the file is renamed', async () => { await test.step('Verify the file is renamed', async () => {
await expect(fileToRename).not.toBeAttached() await expect(fileToRename).not.toBeAttached()
await expect(renamedFile).toBeVisible() await expect(renamedFile).toBeVisible()
expect(checkUnRenamedFS()).toBeFalsy()
expect(checkRenamedFS()).toBeTruthy()
}) })
await test.step('Verify we navigated', async () => { await test.step('Verify we navigated', async () => {
@ -1421,7 +1429,7 @@ test.describe('Renaming in the file tree', () => {
process.platform === 'win32', process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' 'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
) )
const { electronApp, page } = await setupElectron({ const { electronApp, page, dir } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true }) await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
@ -1448,6 +1456,14 @@ test.describe('Renaming in the file tree', () => {
// Constants and locators // Constants and locators
const newFileName = 'newFileName' const newFileName = 'newFileName'
const checkUnRenamedFS = () => {
const filePath = join(dir, 'Test Project', 'fileToRename.kcl')
return fs.existsSync(filePath)
}
const checkRenamedFS = () => {
const filePath = join(dir, 'Test Project', `${newFileName}.kcl`)
return fs.existsSync(filePath)
}
const projectLink = page.getByText('Test Project') const projectLink = page.getByText('Test Project')
const projectMenuButton = page.getByTestId('project-sidebar-toggle') const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const fileToRename = page const fileToRename = page
@ -1468,6 +1484,8 @@ test.describe('Renaming in the file tree', () => {
await u.openFilePanel() await u.openFilePanel()
await expect(fileToRename).toBeVisible() await expect(fileToRename).toBeVisible()
expect(checkUnRenamedFS()).toBeTruthy()
expect(checkRenamedFS()).toBeFalsy()
}) })
await test.step('Rename the file', async () => { await test.step('Rename the file', async () => {
@ -1481,6 +1499,8 @@ test.describe('Renaming in the file tree', () => {
await test.step('Verify the file is renamed', async () => { await test.step('Verify the file is renamed', async () => {
await expect(fileToRename).not.toBeAttached() await expect(fileToRename).not.toBeAttached()
await expect(renamedFile).toBeVisible() await expect(renamedFile).toBeVisible()
expect(checkUnRenamedFS()).toBeFalsy()
expect(checkRenamedFS()).toBeTruthy()
}) })
await test.step('Verify we have not navigated', async () => { await test.step('Verify we have not navigated', async () => {
@ -1511,7 +1531,7 @@ test.describe('Renaming in the file tree', () => {
process.platform === 'win32', process.platform === 'win32',
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' 'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
) )
const { electronApp, page } = await setupElectron({ const { electronApp, page, dir } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true }) await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
@ -1548,8 +1568,17 @@ test.describe('Renaming in the file tree', () => {
}) })
const renamedFolder = page.getByRole('button', { name: 'newFolderName' }) const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
const renameMenuItem = page.getByRole('button', { name: 'Rename' }) const renameMenuItem = page.getByRole('button', { name: 'Rename' })
const renameInput = page.getByPlaceholder('folderToRename') const originalFolderName = 'folderToRename'
const renameInput = page.getByPlaceholder(originalFolderName)
const newFolderName = 'newFolderName' const newFolderName = 'newFolderName'
const checkUnRenamedFolderFS = () => {
const folderPath = join(dir, 'Test Project', originalFolderName)
return fs.existsSync(folderPath)
}
const checkRenamedFolderFS = () => {
const folderPath = join(dir, 'Test Project', newFolderName)
return fs.existsSync(folderPath)
}
await test.step('Open project and file pane', async () => { await test.step('Open project and file pane', async () => {
await expect(projectLink).toBeVisible() await expect(projectLink).toBeVisible()
@ -1563,6 +1592,8 @@ test.describe('Renaming in the file tree', () => {
await u.openFilePanel() await u.openFilePanel()
await expect(folderToRename).toBeVisible() await expect(folderToRename).toBeVisible()
expect(checkUnRenamedFolderFS()).toBeTruthy()
expect(checkRenamedFolderFS()).toBeFalsy()
}) })
await test.step('Rename the folder', async () => { await test.step('Rename the folder', async () => {
@ -1582,6 +1613,8 @@ test.describe('Renaming in the file tree', () => {
await expect(projectMenuButton).toContainText('main.kcl') await expect(projectMenuButton).toContainText('main.kcl')
await expect(renamedFolder).toBeVisible() await expect(renamedFolder).toBeVisible()
await expect(folderToRename).not.toBeAttached() await expect(folderToRename).not.toBeAttached()
expect(checkUnRenamedFolderFS()).toBeFalsy()
expect(checkRenamedFolderFS()).toBeTruthy()
}) })
await electronApp.close() await electronApp.close()
@ -1597,7 +1630,7 @@ test.describe('Renaming in the file tree', () => {
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' 'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
) )
const exampleDir = join('src', 'wasm-lib', 'tests', 'executor', 'inputs') const exampleDir = join('src', 'wasm-lib', 'tests', 'executor', 'inputs')
const { electronApp, page } = await setupElectron({ const { electronApp, page, dir } = await setupElectron({
testInfo, testInfo,
folderSetupFn: async (dir) => { folderSetupFn: async (dir) => {
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true }) await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
@ -1630,8 +1663,17 @@ test.describe('Renaming in the file tree', () => {
has: page.getByRole('button', { name: 'someFileWithin.kcl' }), has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
}) })
const renameMenuItem = page.getByRole('button', { name: 'Rename' }) const renameMenuItem = page.getByRole('button', { name: 'Rename' })
const renameInput = page.getByPlaceholder('folderToRename') const originalFolderName = 'folderToRename'
const renameInput = page.getByPlaceholder(originalFolderName)
const newFolderName = 'newFolderName' const newFolderName = 'newFolderName'
const checkUnRenamedFolderFS = () => {
const folderPath = join(dir, 'Test Project', originalFolderName)
return fs.existsSync(folderPath)
}
const checkRenamedFolderFS = () => {
const folderPath = join(dir, 'Test Project', newFolderName)
return fs.existsSync(folderPath)
}
await test.step('Open project and navigate into folder', async () => { await test.step('Open project and navigate into folder', async () => {
await expect(projectLink).toBeVisible() await expect(projectLink).toBeVisible()
@ -1654,6 +1696,8 @@ test.describe('Renaming in the file tree', () => {
expect(newUrl).toContain('folderToRename') expect(newUrl).toContain('folderToRename')
expect(newUrl).toContain('someFileWithin.kcl') expect(newUrl).toContain('someFileWithin.kcl')
expect(newUrl).not.toContain('main.kcl') expect(newUrl).not.toContain('main.kcl')
expect(checkUnRenamedFolderFS()).toBeTruthy()
expect(checkRenamedFolderFS()).toBeFalsy()
}) })
await test.step('Rename the folder', async () => { await test.step('Rename the folder', async () => {
@ -1680,6 +1724,8 @@ test.describe('Renaming in the file tree', () => {
expect(url).not.toContain('main.kcl') expect(url).not.toContain('main.kcl')
expect(url).toContain(newFolderName) expect(url).toContain(newFolderName)
expect(url).toContain('someFileWithin.kcl') expect(url).toContain('someFileWithin.kcl')
expect(checkUnRenamedFolderFS()).toBeFalsy()
expect(checkRenamedFolderFS()).toBeTruthy()
}) })
await electronApp.close() await electronApp.close()
@ -1687,6 +1733,78 @@ test.describe('Renaming in the file tree', () => {
) )
}) })
test.describe('Deleting files from the file pane', () => {
test(
`when main.kcl exists, navigate to main.kcl`,
{ 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 })
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
`${dir}/testProject/main.kcl`
)
await fsp.copyFile(
'src/wasm-lib/tests/executor/inputs/basic_fillet_cube_end.kcl',
`${dir}/testProject/fileToDelete.kcl`
)
},
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// Constants and locators
const projectCard = page.getByText('testProject')
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const fileToDelete = page
.getByRole('listitem')
.filter({ has: page.getByRole('button', { name: 'fileToDelete.kcl' }) })
const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
const deleteConfirmation = page.getByTestId('delete-confirmation')
await test.step('Open project and navigate to fileToDelete.kcl', async () => {
await projectCard.click()
await u.waitForPageLoad()
await u.openFilePanel()
await fileToDelete.click()
await u.waitForPageLoad()
await u.openKclCodePanel()
await expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
await u.closeKclCodePanel()
})
await test.step('Delete fileToDelete.kcl', async () => {
await fileToDelete.click({ button: 'right' })
await expect(deleteMenuItem).toBeVisible()
await deleteMenuItem.click()
await expect(deleteConfirmation).toBeVisible()
await deleteConfirmation.click()
})
await test.step('Check deletion and navigation', async () => {
await u.waitForPageLoad()
await expect(fileToDelete).not.toBeVisible()
await u.closeFilePanel()
await u.openKclCodePanel()
await expect(u.codeLocator).toContainText('circle(')
await expect(projectMenuButton).toContainText('main.kcl')
})
await electronApp.close()
}
)
test.fixme('TODO - when main.kcl does not exist', async () => {})
})
test( test(
'Original project name persist after onboarding', 'Original project name persist after onboarding',
{ tag: '@electron' }, { tag: '@electron' },

View File

@ -9,7 +9,6 @@ import {
test, test,
} from '@playwright/test' } from '@playwright/test'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import os from 'os'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import fsSync from 'fs' import fsSync from 'fs'
import { join } from 'path' import { join } from 'path'
@ -78,11 +77,10 @@ async function waitForPageLoad(page: Page) {
} }
async function removeCurrentCode(page: Page) { async function removeCurrentCode(page: Page) {
const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control'
await page.locator('.cm-content').click() await page.locator('.cm-content').click()
await page.keyboard.down(hotkey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('a') await page.keyboard.press('a')
await page.keyboard.up(hotkey) await page.keyboard.up('ControlOrMeta')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
await expect(page.locator('.cm-content')).toHaveText('') await expect(page.locator('.cm-content')).toHaveText('')
} }
@ -745,11 +743,6 @@ export const doExport = async (
} }
} }
/**
* Gets the appropriate modifier key for the platform.
*/
export const metaModifier = os.platform() === 'darwin' ? 'Meta' : 'Control'
export async function tearDown(page: Page, testInfo: TestInfo) { export async function tearDown(page: Page, testInfo: TestInfo) {
if (testInfo.status === 'skipped') return if (testInfo.status === 'skipped') return
if (testInfo.status === 'failed') return if (testInfo.status === 'failed') return
@ -872,7 +865,7 @@ export async function setupElectron({
await setup(context, page) await setup(context, page)
return { electronApp, page } return { electronApp, page, dir: projectDirName }
} }
export async function isOutOfViewInScrollContainer( export async function isOutOfViewInScrollContainer(

View File

@ -72,7 +72,7 @@ test.describe('Testing settings', () => {
const inputLocator = page.locator('input[name="modeling-showDebugPanel"]') const inputLocator = page.locator('input[name="modeling-showDebugPanel"]')
// Open the settings modal with the browser keyboard shortcut // Open the settings modal with the browser keyboard shortcut
await page.keyboard.press('Meta+Shift+,') await page.keyboard.press('ControlOrMeta+Shift+,')
await expect(headingLocator).toBeVisible() await expect(headingLocator).toBeVisible()
await page.locator('#showDebugPanel').getByText('OffOn').click() await page.locator('#showDebugPanel').getByText('OffOn').click()
@ -82,7 +82,7 @@ test.describe('Testing settings', () => {
await test.step('Open settings with keyboard shortcut', async () => { await test.step('Open settings with keyboard shortcut', async () => {
await page.getByTestId('settings-close-button').click() await page.getByTestId('settings-close-button').click()
await page.locator('.cm-content').click() await page.locator('.cm-content').click()
await page.keyboard.press('Meta+Shift+,') await page.keyboard.press('ControlOrMeta+Shift+,')
await expect(headingLocator).toBeVisible() await expect(headingLocator).toBeVisible()
}) })

View File

@ -9,8 +9,6 @@ test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo) await tearDown(page, testInfo)
}) })
const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control'
test.describe('Text-to-CAD tests', () => { test.describe('Text-to-CAD tests', () => {
test('basic lego happy case', async ({ page }) => { test('basic lego happy case', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -298,9 +296,9 @@ test.describe('Text-to-CAD tests', () => {
await expect(page.locator('textarea')).toContainText(badPrompt) await expect(page.locator('textarea')).toContainText(badPrompt)
// Select all and start a new prompt. // Select all and start a new prompt.
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyA') await page.keyboard.press('KeyA')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
await page.keyboard.type('a 2x4 lego') await page.keyboard.type('a 2x4 lego')
// Submit the new prompt. // Submit the new prompt.
@ -520,9 +518,9 @@ test.describe('Text-to-CAD tests', () => {
await page.locator('.cm-content').click({ position: { x: 10, y: 10 } }) await page.locator('.cm-content').click({ position: { x: 10, y: 10 } })
// Paste the code. // Paste the code.
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyV') await page.keyboard.press('KeyV')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
// Expect the code to be pasted. // Expect the code to be pasted.
await expect(page.locator('.cm-content')).toContainText(`2x8`) await expect(page.locator('.cm-content')).toContainText(`2x8`)
@ -549,13 +547,13 @@ test.describe('Text-to-CAD tests', () => {
await page.locator('.cm-content').click({ position: { x: 10, y: 10 } }) await page.locator('.cm-content').click({ position: { x: 10, y: 10 } })
// Paste the code. // Paste the code.
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyA') await page.keyboard.press('KeyA')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyV') await page.keyboard.press('KeyV')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
// Expect the code to be pasted. // Expect the code to be pasted.
await expect(page.locator('.cm-content')).toContainText(`2x4`) await expect(page.locator('.cm-content')).toContainText(`2x4`)
@ -636,9 +634,9 @@ test.describe('Text-to-CAD tests', () => {
await page.locator('.cm-content').click({ position: { x: 10, y: 10 } }) await page.locator('.cm-content').click({ position: { x: 10, y: 10 } })
// Paste the code. // Paste the code.
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyV') await page.keyboard.press('KeyV')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
// Expect the code to be pasted. // Expect the code to be pasted.
await expect(page.locator('.cm-content')).toContainText(`2x4`) await expect(page.locator('.cm-content')).toContainText(`2x4`)

View File

@ -1,13 +1,6 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { import { doExport, getUtils, makeTemplate, setup, tearDown } from './test-utils'
doExport,
getUtils,
makeTemplate,
metaModifier,
setup,
tearDown,
} from './test-utils'
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page) await setup(context, page)
@ -17,8 +10,6 @@ test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo) await tearDown(page, testInfo)
}) })
const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control'
test('Units menu', async ({ page }) => { test('Units menu', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -157,7 +148,7 @@ test('Paste should not work unless an input is focused', async ({
// Paste without the code pane focused // Paste without the code pane focused
await codeEditorText.blur() await codeEditorText.blur()
await page.keyboard.press(`${metaModifier}+KeyV`) await page.keyboard.press('ControlOrMeta+KeyV')
// Show that the paste didn't work but typing did // Show that the paste didn't work but typing did
await expect(codeEditorText).not.toContainText(pasteContent) await expect(codeEditorText).not.toContainText(pasteContent)
@ -166,7 +157,7 @@ test('Paste should not work unless an input is focused', async ({
// Paste with the code editor focused // Paste with the code editor focused
// Following this guidance: https://github.com/microsoft/playwright/issues/8114 // Following this guidance: https://github.com/microsoft/playwright/issues/8114
await codeEditorText.focus() await codeEditorText.focus()
await page.keyboard.press(`${metaModifier}+KeyV`) await page.keyboard.press('ControlOrMeta+KeyV')
await expect( await expect(
await page.evaluate( await page.evaluate(
() => document.querySelector('.cm-content')?.textContent () => document.querySelector('.cm-content')?.textContent
@ -380,9 +371,9 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
await test.step(`Type code with sketch hotkeys, shouldn't fire`, async () => { await test.step(`Type code with sketch hotkeys, shouldn't fire`, async () => {
// Since there's code now, we have to get to the end of the line // Since there's code now, we have to get to the end of the line
await page.locator('.cm-line').last().click() await page.locator('.cm-line').last().click()
await page.keyboard.down(CtrlKey) await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('ArrowRight') await page.keyboard.press('ArrowRight')
await page.keyboard.up(CtrlKey) await page.keyboard.up('ControlOrMeta')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page.keyboard.type('//') await page.keyboard.type('//')

View File

@ -190,7 +190,7 @@ function CoreDump() {
() => new CoreDumpManager(engineCommandManager, codeManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
[] []
) )
useHotkeyWrapper(['meta + shift + .'], () => { useHotkeyWrapper(['mod + shift + .'], () => {
toast.promise( toast.promise(
coreDump(coreDumpManager, true), coreDump(coreDumpManager, true),
{ {

View File

@ -396,8 +396,8 @@ export const FileTreeMenu = () => {
}) })
} }
useHotkeyWrapper(['meta + n'], createFile) useHotkeyWrapper(['mod + n'], createFile)
useHotkeyWrapper(['meta + shift + n'], createFolder) useHotkeyWrapper(['mod + shift + n'], createFolder)
return ( return (
<> <>

View File

@ -90,12 +90,12 @@ export const processMemory = (programMemory: ProgramMemory) => {
for (const [key, val] of programMemory?.visibleEntries()) { for (const [key, val] of programMemory?.visibleEntries()) {
if (typeof val.value !== 'function') { if (typeof val.value !== 'function') {
const sg = sketchGroupFromKclValue(val, null) const sg = sketchGroupFromKclValue(val, null)
if (!err(sg)) { if (val.type === 'ExtrudeGroup') {
processedMemory[key] = sg.value.map(({ __geoMeta, ...rest }: Path) => { processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest return rest
}) })
} else if (val.type === 'ExtrudeGroup') { } else if (!err(sg)) {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => { processedMemory[key] = sg.value.map(({ __geoMeta, ...rest }: Path) => {
return rest return rest
}) })
} else if ((val.type as any) === 'Function') { } else if ((val.type as any) === 'Function') {

View File

@ -1,44 +1,11 @@
import { isDesktop } from 'lib/isDesktop' import { Platform, platform } from 'lib/utils'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
export type Platform = 'macos' | 'windows' | 'linux' | ''
export default function usePlatform() { export default function usePlatform() {
const [platformName, setPlatformName] = useState<Platform>('') const [platformName, setPlatformName] = useState<Platform>('')
useEffect(() => { useEffect(() => {
function getPlatform(): Platform { setPlatformName(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()) {
setPlatformName(getPlatform())
} else {
if (navigator.userAgent.indexOf('Mac') !== -1) {
setPlatformName('macos')
} else if (navigator.userAgent.indexOf('Win') !== -1) {
setPlatformName('windows')
} else if (navigator.userAgent.indexOf('Linux') !== -1) {
setPlatformName('linux')
}
}
}, [setPlatformName]) }, [setPlatformName])
return platformName return platformName

View File

@ -338,13 +338,16 @@ export function sketchGroupFromKclValue(
varName: string | null varName: string | null
): SketchGroup | Error { ): SketchGroup | Error {
if (obj?.value?.type === 'SketchGroup') return obj.value if (obj?.value?.type === 'SketchGroup') return obj.value
if (obj?.value?.type === 'ExtrudeGroup') return obj.value.sketchGroup
if (obj?.type === 'ExtrudeGroup') return obj.sketchGroup
if (!varName) { if (!varName) {
varName = 'a KCL value' varName = 'a KCL value'
} }
const actualType = obj?.value?.type ?? obj?.type const actualType = obj?.value?.type ?? obj?.type
if (actualType) { if (actualType) {
console.log(obj)
return new Error( return new Error(
`Expected ${varName} to be a sketchGroup, but it was ${actualType} instead.` `Expected ${varName} to be a sketchGroup or extrudeGroup, but it was ${actualType} instead.`
) )
} else { } else {
return new Error(`Expected ${varName} to be a sketchGroup, but it wasn't.`) return new Error(`Expected ${varName} to be a sketchGroup, but it wasn't.`)

View File

@ -109,11 +109,13 @@ export class CoreDumpManager {
getWebrtcStats(): Promise<string> { getWebrtcStats(): Promise<string> {
if (!this.engineCommandManager.engineConnection) { if (!this.engineCommandManager.engineConnection) {
throw new Error('Engine connection not initialized') // when the engine connection is not available, return an empty object.
return Promise.resolve(JSON.stringify({}))
} }
if (!this.engineCommandManager.engineConnection.webrtcStatsCollector) { if (!this.engineCommandManager.engineConnection.webrtcStatsCollector) {
throw new Error('Engine webrtcStatsCollector not initialized') // when the engine connection is not available, return an empty object.
return Promise.resolve(JSON.stringify({}))
} }
return this.engineCommandManager.engineConnection return this.engineCommandManager.engineConnection

View File

@ -441,28 +441,34 @@ export const readProjectSettingsFile = async (
return configObj return configObj
} }
/**
* Read the app settings file, or creates an initial one if it doesn't exist.
*/
export const readAppSettingsFile = async () => { export const readAppSettingsFile = async () => {
let settingsPath = await getAppSettingsFilePath() let settingsPath = await getAppSettingsFilePath()
try {
await window.electron.stat(settingsPath)
} catch (e) {
if (e === 'ENOENT') {
const config = defaultAppSettings()
if (err(config)) return Promise.reject(config)
if (!config.settings?.app)
return Promise.reject(new Error('config.app is falsey'))
config.settings.app.project_directory = await getInitialDefaultDir() // The file exists, read it and parse it.
return config if (window.electron.exists(settingsPath)) {
const configToml = await window.electron.readFile(settingsPath)
const configObj = parseAppSettings(configToml)
if (err(configObj)) {
return Promise.reject(configObj)
} }
}
const configToml = await window.electron.readFile(settingsPath) return configObj
const configObj = parseAppSettings(configToml)
if (err(configObj)) {
return Promise.reject(configObj)
} }
return configObj // The file doesn't exist, create a new one.
// This defaultAppConfig is truly an empty object every time.
const defaultAppConfig = defaultAppSettings()
if (err(defaultAppConfig)) {
return Promise.reject(defaultAppConfig)
}
const initialDirConfig: DeepPartial<Configuration> = {
settings: { project: { directory: await getInitialDefaultDir() } },
}
const config = Object.assign(defaultAppConfig, initialDirConfig)
return config
} }
export const writeAppSettingsFile = async (tomlStr: string) => { export const writeAppSettingsFile = async (tomlStr: string) => {

View File

@ -31,7 +31,7 @@ function mapHotkeyToCodeMirrorHotkey(hotkey: string): string {
return hotkey return hotkey
.replaceAll('+', '-') .replaceAll('+', '-')
.replaceAll(' ', '') .replaceAll(' ', '')
.replaceAll('mod', 'Meta') .replaceAll('mod', 'Mod')
.replaceAll('meta', 'Meta') .replaceAll('meta', 'Meta')
.replaceAll('ctrl', 'Ctrl') .replaceAll('ctrl', 'Ctrl')
.replaceAll('shift', 'Shift') .replaceAll('shift', 'Shift')

View File

@ -1,4 +1,5 @@
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { platform } from 'lib/utils'
export type InteractionMapItem = { export type InteractionMapItem = {
name: string name: string
@ -24,6 +25,11 @@ export const interactionMapCategories = [
type InteractionMapCategory = (typeof interactionMapCategories)[number] type InteractionMapCategory = (typeof interactionMapCategories)[number]
/**
* Primary modifier key for the current platform.
*/
const PRIMARY = platform() === 'macos' ? 'Command' : 'Control'
/** /**
* A temporary implementation of the interaction map for * A temporary implementation of the interaction map for
* display purposes only. * display purposes only.
@ -38,7 +44,7 @@ export const interactionMap: Record<
Settings: [ Settings: [
{ {
name: 'toggle-settings', name: 'toggle-settings',
sequence: isDesktop() ? 'Meta+,' : 'Shift+Meta+,', sequence: isDesktop() ? `${PRIMARY}+,` : `Shift+${PRIMARY}+,`,
title: 'Toggle Settings', title: 'Toggle Settings',
description: 'Opens the settings dialog. Always available.', description: 'Opens the settings dialog. Always available.',
}, },
@ -53,7 +59,7 @@ export const interactionMap: Record<
'Command Palette': [ 'Command Palette': [
{ {
name: 'toggle-command-palette', name: 'toggle-command-palette',
sequence: 'Meta+K', sequence: `${PRIMARY}+K`,
title: 'Toggle Command Palette', title: 'Toggle Command Palette',
description: 'Always available. Use Ctrl+/ on Windows/Linux.', description: 'Always available. Use Ctrl+/ on Windows/Linux.',
}, },
@ -159,7 +165,7 @@ export const interactionMap: Record<
}, },
{ {
name: 'delete-file', name: 'delete-file',
sequence: 'Meta+Backspace', sequence: `${PRIMARY}+Backspace`,
title: 'Delete File/Folder', title: 'Delete File/Folder',
description: description:
'Available when a file or folder is selected in the file tree.', 'Available when a file or folder is selected in the file tree.',

View File

@ -1,6 +1,7 @@
import { SourceRange } from '../lang/wasm' import { SourceRange } from '../lang/wasm'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { isDesktop } from './isDesktop'
export const uuidv4 = v4 export const uuidv4 = v4
@ -126,6 +127,41 @@ export function getNormalisedCoordinates({
} }
} }
// TODO: Remove the empty platform type.
export type Platform = 'macos' | 'windows' | 'linux' | ''
export function platform(): Platform {
if (isDesktop()) {
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 (navigator.userAgent.indexOf('Mac') !== -1) {
return 'macos'
} else if (navigator.userAgent.indexOf('Win') !== -1) {
return 'windows'
} else if (navigator.userAgent.indexOf('Linux') !== -1) {
return 'linux'
}
console.error('Unknown platform userAgent:', navigator.userAgent)
return ''
}
export function isReducedMotion(): boolean { export function isReducedMotion(): boolean {
return ( return (
typeof window !== 'undefined' && typeof window !== 'undefined' &&

View File

@ -230,29 +230,42 @@ pub struct OsInfo {
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct WebrtcStats { pub struct WebrtcStats {
/// The packets lost. /// The packets lost.
pub packets_lost: u32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub packets_lost: Option<u32>,
/// The frames received. /// The frames received.
pub frames_received: u32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub frames_received: Option<u32>,
/// The frame width. /// The frame width.
pub frame_width: f32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub frame_width: Option<f32>,
/// The frame height. /// The frame height.
pub frame_height: f32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub frame_height: Option<f32>,
/// The frame rate. /// The frame rate.
pub frame_rate: f32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub frame_rate: Option<f32>,
/// The number of key frames decoded. /// The number of key frames decoded.
pub key_frames_decoded: u32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub key_frames_decoded: Option<u32>,
/// The number of frames dropped. /// The number of frames dropped.
pub frames_dropped: u32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub frames_dropped: Option<u32>,
/// The pause count. /// The pause count.
pub pause_count: u32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub pause_count: Option<u32>,
/// The total pauses duration. /// The total pauses duration.
pub total_pauses_duration: f32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub total_pauses_duration: Option<f32>,
/// The freeze count. /// The freeze count.
pub freeze_count: u32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub freeze_count: Option<u32>,
/// The total freezes duration. /// The total freezes duration.
pub total_freezes_duration: f32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub total_freezes_duration: Option<f32>,
/// The pli count. /// The pli count.
pub pli_count: u32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub pli_count: Option<u32>,
/// Packet jitter for this synchronizing source, measured in seconds. /// Packet jitter for this synchronizing source, measured in seconds.
pub jitter: f32, #[serde(default, skip_serializing_if = "Option::is_none")]
pub jitter: Option<f32>,
} }