Should exit sketchMode when creating new file in the file tree pane (#3993)

* fix new file sketch mode issue

* initial extron app fixture

* Add tests for exiting sketch mode on file tree actions

* organise files

* before all after all clean up

* tweak after each

* makes typedKeys as unsafe

* update mask for draft line snapshots

* fix mask

* add fix again
This commit is contained in:
Kurt Hutten
2024-10-01 07:56:04 +10:00
committed by GitHub
parent 8cb17a8936
commit 5112b48324
23 changed files with 753 additions and 357 deletions

View File

@ -1,4 +1,5 @@
import { test, expect } from '@playwright/test' import { _test, _expect } from './playwright-deprecated'
import { test, expect } from './fixtures/fixtureSetup'
import * as fsp from 'fs/promises' import * as fsp from 'fs/promises'
import * as fs from 'fs' import * as fs from 'fs'
import { import {
@ -11,14 +12,98 @@ import {
import { join } from 'path' import { join } from 'path'
import { FILE_EXT } from 'lib/constants' import { FILE_EXT } from 'lib/constants'
test.beforeEach(async ({ context, page }, testInfo) => { _test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo) await setup(context, page, testInfo)
}) })
test.afterEach(async ({ page }, testInfo) => { _test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo) await tearDown(page, testInfo)
}) })
test.describe('integrations tests', () => {
test(
'Creating a new file or switching file while in sketchMode should exit sketchMode',
{ tag: '@electron' },
async ({ tronApp, homePage, scene, editor, toolbar }) => {
test.skip(
process.platform === 'win32',
'windows times out will waiting for the execution indicator?'
)
await tronApp.initialise({
fixtures: { homePage, scene, editor, toolbar },
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'test-sample')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
join(bracketDir, 'main.kcl')
)
},
})
const [clickObj] = await scene.makeMouseHelpers(600, 300)
await test.step('setup test', async () => {
await homePage.expectState({
projectCards: [
{
title: 'test-sample',
fileCount: 1,
folderCount: 1,
},
],
sortBy: 'last-modified-desc',
})
await homePage.openProject('test-sample')
// windows times out here, hence the skip above
await scene.waitForExecutionDone()
})
await test.step('enter sketch mode', async () => {
await clickObj()
await scene.moveNoWhere()
await editor.expectState({
activeLines: [
'|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]',
],
highlightedCode: '',
diagnostics: [],
})
await toolbar.editSketch()
await expect(toolbar.exitSketchBtn).toBeVisible()
})
await test.step('check sketch mode is exited when creating new file', async () => {
await toolbar.fileTreeBtn.click()
await toolbar.expectFileTreeState(['main.kcl'])
await toolbar.createFile({ wait: true })
// check we're out of sketch mode
await expect(toolbar.exitSketchBtn).not.toBeVisible()
await expect(toolbar.startSketchBtn).toBeVisible()
})
await test.step('setup for next assertion', async () => {
await toolbar.openFile('main.kcl')
await clickObj()
await scene.moveNoWhere()
await editor.expectState({
activeLines: [
'|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]',
],
highlightedCode: '',
diagnostics: [],
})
await toolbar.editSketch()
await expect(toolbar.exitSketchBtn).toBeVisible()
await toolbar.expectFileTreeState(['main.kcl', 'Untitled.kcl'])
})
await test.step('check sketch mode is exited when opening a different file', async () => {
await toolbar.openFile('untitled.kcl', { wait: false })
// check we're out of sketch mode
await expect(toolbar.exitSketchBtn).not.toBeVisible()
await expect(toolbar.startSketchBtn).toBeVisible()
})
}
)
})
test.describe('when using the file tree to', () => { test.describe('when using the file tree to', () => {
const fromFile = 'main.kcl' const fromFile = 'main.kcl'
const toFile = 'hello.kcl' const toFile = 'hello.kcl'
@ -26,11 +111,8 @@ test.describe('when using the file tree to', () => {
test( test(
`rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`, `rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _, tronApp }, testInfo) => {
const { electronApp, page } = await setupElectron({ await tronApp.initialise()
testInfo,
folderSetupFn: async () => {},
})
const { const {
panesOpen, panesOpen,
@ -38,10 +120,10 @@ test.describe('when using the file tree to', () => {
pasteCodeInEditor, pasteCodeInEditor,
renameFile, renameFile,
editorTextMatches, editorTextMatches,
} = await getUtils(page, test) } = await getUtils(tronApp.page, test)
await page.setViewportSize({ width: 1200, height: 500 }) await tronApp.page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) tronApp.page.on('console', console.log)
await panesOpen(['files', 'code']) await panesOpen(['files', 'code'])
@ -55,39 +137,38 @@ test.describe('when using the file tree to', () => {
await pasteCodeInEditor(kclCube) await pasteCodeInEditor(kclCube)
await renameFile(fromFile, toFile) await renameFile(fromFile, toFile)
await page.reload() await tronApp.page.reload()
await test.step('Postcondition: editor has same content as before the rename', async () => { await test.step('Postcondition: editor has same content as before the rename', async () => {
await editorTextMatches(kclCube) await editorTextMatches(kclCube)
}) })
await test.step('Postcondition: opening and closing settings works', async () => { await test.step('Postcondition: opening and closing settings works', async () => {
const settingsOpenButton = page.getByRole('link', { const settingsOpenButton = tronApp.page.getByRole('link', {
name: 'settings Settings', name: 'settings Settings',
}) })
const settingsCloseButton = page.getByTestId('settings-close-button') const settingsCloseButton = tronApp.page.getByTestId(
'settings-close-button'
)
await settingsOpenButton.click() await settingsOpenButton.click()
await settingsCloseButton.click() await settingsCloseButton.click()
}) })
await electronApp.close() await tronApp.close()
} }
) )
test( test(
`create many new untitled files they increment their names`, `create many new untitled files they increment their names`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _, tronApp }, testInfo) => {
const { electronApp, page } = await setupElectron({ await tronApp.initialise()
testInfo,
folderSetupFn: async () => {},
})
const { panesOpen, createAndSelectProject, createNewFile } = const { panesOpen, createAndSelectProject, createNewFile } =
await getUtils(page, test) await getUtils(tronApp.page, test)
await page.setViewportSize({ width: 1200, height: 500 }) await tronApp.page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) tronApp.page.on('console', console.log)
await panesOpen(['files']) await panesOpen(['files'])
@ -101,23 +182,21 @@ test.describe('when using the file tree to', () => {
await test.step('Postcondition: there are 5 new Untitled-*.kcl files', async () => { await test.step('Postcondition: there are 5 new Untitled-*.kcl files', async () => {
await expect( await expect(
page tronApp.page
.locator('[data-testid="file-pane-scroll-container"] button') .locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: /Untitled[-]?[0-5]?/ }) .filter({ hasText: /Untitled[-]?[0-5]?/ })
).toHaveCount(5) ).toHaveCount(5)
}) })
await electronApp.close() await tronApp.close()
} }
) )
test( test(
'create a new file with the same name as an existing file cancels the operation', 'create a new file with the same name as an existing file cancels the operation',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _, tronApp }, testInfo) => {
const { electronApp, page } = await setupElectron({ await tronApp.initialise()
testInfo,
})
const { const {
openKclCodePanel, openKclCodePanel,
@ -128,10 +207,10 @@ test.describe('when using the file tree to', () => {
renameFile, renameFile,
selectFile, selectFile,
editorTextMatches, editorTextMatches,
} = await getUtils(page, test) } = await getUtils(tronApp.page, _test)
await page.setViewportSize({ width: 1200, height: 500 }) await tronApp.page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) tronApp.page.on('console', console.log)
await createAndSelectProject('project-000') await createAndSelectProject('project-000')
await openKclCodePanel() await openKclCodePanel()
@ -159,25 +238,22 @@ test.describe('when using the file tree to', () => {
await selectFile(kcl1) await selectFile(kcl1)
await editorTextMatches(kclCube) await editorTextMatches(kclCube)
}) })
await page.waitForTimeout(500) await tronApp.page.waitForTimeout(500)
await test.step(`Postcondition: ${kcl2} still exists with the original content`, async () => { await test.step(`Postcondition: ${kcl2} still exists with the original content`, async () => {
await selectFile(kcl2) await selectFile(kcl2)
await editorTextMatches(kclCylinder) await editorTextMatches(kclCylinder)
}) })
await electronApp.close() await tronApp?.close?.()
} }
) )
test( test(
'deleting all files recreates a default main.kcl with no code', 'deleting all files recreates a default main.kcl with no code',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _, tronApp }, testInfo) => {
const { electronApp, page } = await setupElectron({ await tronApp.initialise()
testInfo,
folderSetupFn: async () => {},
})
const { const {
panesOpen, panesOpen,
@ -185,10 +261,10 @@ test.describe('when using the file tree to', () => {
pasteCodeInEditor, pasteCodeInEditor,
deleteFile, deleteFile,
editorTextMatches, editorTextMatches,
} = await getUtils(page, test) } = await getUtils(tronApp.page, _test)
await page.setViewportSize({ width: 1200, height: 500 }) await tronApp.page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) tronApp.page.on('console', console.log)
await panesOpen(['files', 'code']) await panesOpen(['files', 'code'])
@ -208,7 +284,7 @@ test.describe('when using the file tree to', () => {
await editorTextMatches('') await editorTextMatches('')
}) })
await electronApp.close() await tronApp.close()
} }
) )
@ -217,10 +293,8 @@ test.describe('when using the file tree to', () => {
{ {
tag: '@electron', tag: '@electron',
}, },
async ({ browser: _ }, testInfo) => { async ({ browser: _, tronApp }, testInfo) => {
const { page } = await setupElectron({ await tronApp.initialise()
testInfo,
})
const { const {
panesOpen, panesOpen,
@ -230,10 +304,10 @@ test.describe('when using the file tree to', () => {
openDebugPanel, openDebugPanel,
closeDebugPanel, closeDebugPanel,
expectCmdLog, expectCmdLog,
} = await getUtils(page, test) } = await getUtils(tronApp.page, test)
await page.setViewportSize({ width: 1200, height: 500 }) await tronApp.page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) tronApp.page.on('console', console.log)
await panesOpen(['files', 'code']) await panesOpen(['files', 'code'])
await createAndSelectProject('project-000') await createAndSelectProject('project-000')
@ -248,30 +322,30 @@ test.describe('when using the file tree to', () => {
// Create a large lego file // Create a large lego file
await createNewFile('lego') await createNewFile('lego')
const legoFile = page.getByRole('listitem').filter({ const legoFile = tronApp.page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'lego.kcl' }), has: tronApp.page.getByRole('button', { name: 'lego.kcl' }),
}) })
await expect(legoFile).toBeVisible({ timeout: 60_000 }) await _expect(legoFile).toBeVisible({ timeout: 60_000 })
await legoFile.click() await legoFile.click()
const kclLego = await fsp.readFile( const kclLego = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/lego.kcl', 'src/wasm-lib/tests/executor/inputs/lego.kcl',
'utf-8' 'utf-8'
) )
await pasteCodeInEditor(kclLego) await pasteCodeInEditor(kclLego)
const mainFile = page.getByRole('listitem').filter({ const mainFile = tronApp.page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'main.kcl' }), has: tronApp.page.getByRole('button', { name: 'main.kcl' }),
}) })
// Open settings and enable the debug panel // Open settings and enable the debug panel
await page await tronApp.page
.getByRole('link', { .getByRole('link', {
name: 'settings Settings', name: 'settings Settings',
}) })
.click() .click()
await page.locator('#showDebugPanel').getByText('OffOn').click() await tronApp.page.locator('#showDebugPanel').getByText('OffOn').click()
await page.getByTestId('settings-close-button').click() await tronApp.page.getByTestId('settings-close-button').click()
await test.step('swap between small and large files', async () => { await _test.step('swap between small and large files', async () => {
await openDebugPanel() await openDebugPanel()
// Previously created a file so we need to start back at main.kcl // Previously created a file so we need to start back at main.kcl
await mainFile.click() await mainFile.click()
@ -283,12 +357,14 @@ test.describe('when using the file tree to', () => {
await expectCmdLog('[data-message-type="execution-done"]', 60_000) await expectCmdLog('[data-message-type="execution-done"]', 60_000)
await closeDebugPanel() await closeDebugPanel()
}) })
await tronApp.close()
} }
) )
}) })
test.describe('Renaming in the file tree', () => { _test.describe('Renaming in the file tree', () => {
test( _test(
'A file you have open', 'A file you have open',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
@ -333,56 +409,56 @@ test.describe('Renaming in the file tree', () => {
const renameInput = page.getByPlaceholder('fileToRename.kcl') const renameInput = page.getByPlaceholder('fileToRename.kcl')
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 () => {
await expect(projectLink).toBeVisible() await _expect(projectLink).toBeVisible()
await projectLink.click() await projectLink.click()
await expect(projectMenuButton).toBeVisible() await _expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl') await _expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel() await u.openFilePanel()
await expect(fileToRename).toBeVisible() await _expect(fileToRename).toBeVisible()
expect(checkUnRenamedFS()).toBeTruthy() _expect(checkUnRenamedFS()).toBeTruthy()
expect(checkRenamedFS()).toBeFalsy() _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()
await expect(codeLocator).toContainText('circle(') await _expect(codeLocator).toContainText('circle(')
await u.closeKclCodePanel() await u.closeKclCodePanel()
}) })
await test.step('Rename the file', async () => { await _test.step('Rename the file', async () => {
await fileToRename.click({ button: 'right' }) await fileToRename.click({ button: 'right' })
await renameMenuItem.click() await renameMenuItem.click()
await expect(renameInput).toBeVisible() await _expect(renameInput).toBeVisible()
await renameInput.fill(newFileName) await renameInput.fill(newFileName)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
}) })
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(checkUnRenamedFS()).toBeFalsy()
expect(checkRenamedFS()).toBeTruthy() _expect(checkRenamedFS()).toBeTruthy()
}) })
await test.step('Verify we navigated', async () => { await _test.step('Verify we navigated', async () => {
await expect(projectMenuButton).toContainText(newFileName + FILE_EXT) await _expect(projectMenuButton).toContainText(newFileName + FILE_EXT)
const url = page.url() const url = page.url()
expect(url).toContain(newFileName) _expect(url).toContain(newFileName)
await expect(projectMenuButton).not.toContainText('fileToRename.kcl') await _expect(projectMenuButton).not.toContainText('fileToRename.kcl')
await expect(projectMenuButton).not.toContainText('main.kcl') await _expect(projectMenuButton).not.toContainText('main.kcl')
expect(url).not.toContain('fileToRename.kcl') _expect(url).not.toContain('fileToRename.kcl')
expect(url).not.toContain('main.kcl') _expect(url).not.toContain('main.kcl')
await u.openKclCodePanel() await u.openKclCodePanel()
await expect(codeLocator).toContainText('circle(') await _expect(codeLocator).toContainText('circle(')
}) })
await electronApp.close() await electronApp.close()
} }
) )
test( _test(
'A file you do not have open', 'A file you do not have open',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
@ -426,54 +502,54 @@ test.describe('Renaming in the file tree', () => {
const renameInput = page.getByPlaceholder('fileToRename.kcl') const renameInput = page.getByPlaceholder('fileToRename.kcl')
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 () => {
await expect(projectLink).toBeVisible() await _expect(projectLink).toBeVisible()
await projectLink.click() await projectLink.click()
await expect(projectMenuButton).toBeVisible() await _expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl') await _expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel() await u.openFilePanel()
await expect(fileToRename).toBeVisible() await _expect(fileToRename).toBeVisible()
expect(checkUnRenamedFS()).toBeTruthy() _expect(checkUnRenamedFS()).toBeTruthy()
expect(checkRenamedFS()).toBeFalsy() _expect(checkRenamedFS()).toBeFalsy()
}) })
await test.step('Rename the file', async () => { await _test.step('Rename the file', async () => {
await fileToRename.click({ button: 'right' }) await fileToRename.click({ button: 'right' })
await renameMenuItem.click() await renameMenuItem.click()
await expect(renameInput).toBeVisible() await _expect(renameInput).toBeVisible()
await renameInput.fill(newFileName) await renameInput.fill(newFileName)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
}) })
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(checkUnRenamedFS()).toBeFalsy()
expect(checkRenamedFS()).toBeTruthy() _expect(checkRenamedFS()).toBeTruthy()
}) })
await test.step('Verify we have not navigated', async () => { await _test.step('Verify we have not navigated', async () => {
await expect(projectMenuButton).toContainText('main.kcl') await _expect(projectMenuButton).toContainText('main.kcl')
await expect(projectMenuButton).not.toContainText( await _expect(projectMenuButton).not.toContainText(
newFileName + FILE_EXT newFileName + FILE_EXT
) )
await expect(projectMenuButton).not.toContainText('fileToRename.kcl') await _expect(projectMenuButton).not.toContainText('fileToRename.kcl')
const url = page.url() const url = page.url()
expect(url).toContain('main.kcl') _expect(url).toContain('main.kcl')
expect(url).not.toContain(newFileName) _expect(url).not.toContain(newFileName)
expect(url).not.toContain('fileToRename.kcl') _expect(url).not.toContain('fileToRename.kcl')
await u.openKclCodePanel() await u.openKclCodePanel()
await expect(codeLocator).toContainText('fillet(') await _expect(codeLocator).toContainText('fillet(')
}) })
await electronApp.close() await electronApp.close()
} }
) )
test( _test(
`A folder you're not inside`, `A folder you're not inside`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
@ -519,48 +595,51 @@ test.describe('Renaming in the file tree', () => {
return fs.existsSync(folderPath) 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()
await projectLink.click() await projectLink.click()
await expect(projectMenuButton).toBeVisible() await _expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl') await _expect(projectMenuButton).toContainText('main.kcl')
const url = page.url() const url = page.url()
expect(url).toContain('main.kcl') _expect(url).toContain('main.kcl')
expect(url).not.toContain('folderToRename') _expect(url).not.toContain('folderToRename')
await u.openFilePanel() await u.openFilePanel()
await expect(folderToRename).toBeVisible() await _expect(folderToRename).toBeVisible()
expect(checkUnRenamedFolderFS()).toBeTruthy() _expect(checkUnRenamedFolderFS()).toBeTruthy()
expect(checkRenamedFolderFS()).toBeFalsy() _expect(checkRenamedFolderFS()).toBeFalsy()
}) })
await test.step('Rename the folder', async () => { await _test.step('Rename the folder', async () => {
await folderToRename.click({ button: 'right' }) await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible() await _expect(renameMenuItem).toBeVisible()
await renameMenuItem.click() await renameMenuItem.click()
await expect(renameInput).toBeVisible() await _expect(renameInput).toBeVisible()
await renameInput.fill(newFolderName) await renameInput.fill(newFolderName)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
}) })
await test.step('Verify the folder is renamed, and no navigation occurred', async () => { await _test.step(
const url = page.url() 'Verify the folder is renamed, and no navigation occurred',
expect(url).toContain('main.kcl') async () => {
expect(url).not.toContain('folderToRename') const url = page.url()
_expect(url).toContain('main.kcl')
_expect(url).not.toContain('folderToRename')
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(checkUnRenamedFolderFS()).toBeFalsy()
expect(checkRenamedFolderFS()).toBeTruthy() _expect(checkRenamedFolderFS()).toBeTruthy()
}) }
)
await electronApp.close() await electronApp.close()
} }
) )
test( _test(
`A folder you are inside`, `A folder you are inside`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
@ -609,66 +688,69 @@ test.describe('Renaming in the file tree', () => {
return fs.existsSync(folderPath) 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()
await projectLink.click() await projectLink.click()
await expect(projectMenuButton).toBeVisible() await _expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl') await _expect(projectMenuButton).toContainText('main.kcl')
const url = page.url() const url = page.url()
expect(url).toContain('main.kcl') _expect(url).toContain('main.kcl')
expect(url).not.toContain('folderToRename') _expect(url).not.toContain('folderToRename')
await u.openFilePanel() await u.openFilePanel()
await expect(folderToRename).toBeVisible() await _expect(folderToRename).toBeVisible()
await folderToRename.click() await folderToRename.click()
await expect(fileWithinFolder).toBeVisible() await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click() await fileWithinFolder.click()
await expect(projectMenuButton).toContainText('someFileWithin.kcl') await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
const newUrl = page.url() const newUrl = page.url()
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(checkUnRenamedFolderFS()).toBeTruthy()
expect(checkRenamedFolderFS()).toBeFalsy() _expect(checkRenamedFolderFS()).toBeFalsy()
}) })
await test.step('Rename the folder', async () => { await _test.step('Rename the folder', async () => {
await page.waitForTimeout(60000) await page.waitForTimeout(60000)
await folderToRename.click({ button: 'right' }) await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible() await _expect(renameMenuItem).toBeVisible()
await renameMenuItem.click() await renameMenuItem.click()
await expect(renameInput).toBeVisible() await _expect(renameInput).toBeVisible()
await renameInput.fill(newFolderName) await renameInput.fill(newFolderName)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
}) })
await test.step('Verify the folder is renamed, and navigated to new path', async () => { await _test.step(
const urlSnippet = encodeURIComponent( 'Verify the folder is renamed, and navigated to new path',
join(newFolderName, 'someFileWithin.kcl') async () => {
) const urlSnippet = encodeURIComponent(
await page.waitForURL(new RegExp(urlSnippet)) join(newFolderName, 'someFileWithin.kcl')
await expect(projectMenuButton).toContainText('someFileWithin.kcl') )
await expect(renamedFolder).toBeVisible() await page.waitForURL(new RegExp(urlSnippet))
await expect(folderToRename).not.toBeAttached() 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 // URL is synchronous, so we check the other stuff first
const url = page.url() const url = page.url()
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(checkUnRenamedFolderFS()).toBeFalsy()
expect(checkRenamedFolderFS()).toBeTruthy() _expect(checkRenamedFolderFS()).toBeTruthy()
}) }
)
await electronApp.close() await electronApp.close()
} }
) )
}) })
test.describe('Deleting items from the file pane', () => { _test.describe('Deleting items from the file pane', () => {
test( _test(
`delete file when main.kcl exists, navigate to main.kcl`, `delete file when main.kcl exists, navigate to main.kcl`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
@ -700,45 +782,48 @@ test.describe('Deleting items from the file pane', () => {
const deleteMenuItem = page.getByRole('button', { name: 'Delete' }) const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
const deleteConfirmation = page.getByTestId('delete-confirmation') const deleteConfirmation = page.getByTestId('delete-confirmation')
await test.step('Open project and navigate to fileToDelete.kcl', async () => { await _test.step(
await projectCard.click() 'Open project and navigate to fileToDelete.kcl',
await u.waitForPageLoad() async () => {
await u.openFilePanel() await projectCard.click()
await u.waitForPageLoad()
await u.openFilePanel()
await fileToDelete.click() await fileToDelete.click()
await u.waitForPageLoad() await u.waitForPageLoad()
await u.openKclCodePanel() await u.openKclCodePanel()
await expect(u.codeLocator).toContainText('getOppositeEdge(thing)') await _expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
await u.closeKclCodePanel() await u.closeKclCodePanel()
}) }
)
await test.step('Delete fileToDelete.kcl', async () => { await _test.step('Delete fileToDelete.kcl', async () => {
await fileToDelete.click({ button: 'right' }) await fileToDelete.click({ button: 'right' })
await expect(deleteMenuItem).toBeVisible() await _expect(deleteMenuItem).toBeVisible()
await deleteMenuItem.click() await deleteMenuItem.click()
await expect(deleteConfirmation).toBeVisible() await _expect(deleteConfirmation).toBeVisible()
await deleteConfirmation.click() await deleteConfirmation.click()
}) })
await test.step('Check deletion and navigation', async () => { await _test.step('Check deletion and navigation', async () => {
await u.waitForPageLoad() await u.waitForPageLoad()
await expect(fileToDelete).not.toBeVisible() await _expect(fileToDelete).not.toBeVisible()
await u.closeFilePanel() await u.closeFilePanel()
await u.openKclCodePanel() await u.openKclCodePanel()
await expect(u.codeLocator).toContainText('circle(') await _expect(u.codeLocator).toContainText('circle(')
await expect(projectMenuButton).toContainText('main.kcl') await _expect(projectMenuButton).toContainText('main.kcl')
}) })
await electronApp.close() await electronApp.close()
} }
) )
test.fixme( _test.fixme(
'TODO - delete file we have open when main.kcl does not exist', 'TODO - delete file we have open when main.kcl does not exist',
async () => {} async () => {}
) )
test( _test(
`Delete folder we are not in, don't navigate`, `Delete folder we are not in, don't navigate`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
@ -772,32 +857,32 @@ test.describe('Deleting items from the file pane', () => {
const deleteMenuItem = page.getByRole('button', { name: 'Delete' }) const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
const deleteConfirmation = page.getByTestId('delete-confirmation') const deleteConfirmation = page.getByTestId('delete-confirmation')
await test.step('Open project and open project pane', async () => { await _test.step('Open project and open project pane', async () => {
await projectCard.click() await projectCard.click()
await u.waitForPageLoad() await u.waitForPageLoad()
await expect(projectMenuButton).toContainText('main.kcl') await _expect(projectMenuButton).toContainText('main.kcl')
await u.closeKclCodePanel() await u.closeKclCodePanel()
await u.openFilePanel() await u.openFilePanel()
}) })
await test.step('Delete folderToDelete', async () => { await _test.step('Delete folderToDelete', async () => {
await folderToDelete.click({ button: 'right' }) await folderToDelete.click({ button: 'right' })
await expect(deleteMenuItem).toBeVisible() await _expect(deleteMenuItem).toBeVisible()
await deleteMenuItem.click() await deleteMenuItem.click()
await expect(deleteConfirmation).toBeVisible() await _expect(deleteConfirmation).toBeVisible()
await deleteConfirmation.click() await deleteConfirmation.click()
}) })
await test.step('Check deletion and no navigation', async () => { await _test.step('Check deletion and no navigation', async () => {
await expect(folderToDelete).not.toBeAttached() await _expect(folderToDelete).not.toBeAttached()
await expect(projectMenuButton).toContainText('main.kcl') await _expect(projectMenuButton).toContainText('main.kcl')
}) })
await electronApp.close() await electronApp.close()
} }
) )
test( _test(
`Delete folder we are in, navigate to main.kcl`, `Delete folder we are in, navigate to main.kcl`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
@ -834,36 +919,45 @@ test.describe('Deleting items from the file pane', () => {
const deleteMenuItem = page.getByRole('button', { name: 'Delete' }) const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
const deleteConfirmation = page.getByTestId('delete-confirmation') const deleteConfirmation = page.getByTestId('delete-confirmation')
await test.step('Open project and navigate into folderToDelete', async () => { await _test.step(
await projectCard.click() 'Open project and navigate into folderToDelete',
await u.waitForPageLoad() async () => {
await expect(projectMenuButton).toContainText('main.kcl') await projectCard.click()
await u.closeKclCodePanel() await u.waitForPageLoad()
await u.openFilePanel() await _expect(projectMenuButton).toContainText('main.kcl')
await u.closeKclCodePanel()
await u.openFilePanel()
await folderToDelete.click() await folderToDelete.click()
await expect(fileWithinFolder).toBeVisible() await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click() await fileWithinFolder.click()
await expect(projectMenuButton).toContainText('someFileWithin.kcl') await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
}) }
)
await test.step('Delete folderToDelete', async () => { await _test.step('Delete folderToDelete', async () => {
await folderToDelete.click({ button: 'right' }) await folderToDelete.click({ button: 'right' })
await expect(deleteMenuItem).toBeVisible() await _expect(deleteMenuItem).toBeVisible()
await deleteMenuItem.click() await deleteMenuItem.click()
await expect(deleteConfirmation).toBeVisible() await _expect(deleteConfirmation).toBeVisible()
await deleteConfirmation.click() await deleteConfirmation.click()
}) })
await test.step('Check deletion and navigation to main.kcl', async () => { await _test.step(
await expect(folderToDelete).not.toBeAttached() 'Check deletion and navigation to main.kcl',
await expect(fileWithinFolder).not.toBeAttached() async () => {
await expect(projectMenuButton).toContainText('main.kcl') await _expect(folderToDelete).not.toBeAttached()
}) await _expect(fileWithinFolder).not.toBeAttached()
await _expect(projectMenuButton).toContainText('main.kcl')
}
)
await electronApp.close() await electronApp.close()
} }
) )
test.fixme('TODO - delete folder we are in, with no main.kcl', async () => {}) _test.fixme(
'TODO - delete folder we are in, with no main.kcl',
async () => {}
)
}) })

View File

@ -1,70 +0,0 @@
import type { Page } from '@playwright/test'
import { test as base } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import fsp from 'fs/promises'
import { join } from 'path'
import { CmdBarFixture } from './cmdBarFixture'
import { EditorFixture } from './editorFixture'
import { ToolbarFixture } from './toolbarFixture'
import { SceneFixture } from './sceneFixture'
export class AuthenticatedApp {
public readonly page: Page
constructor(page: Page) {
this.page = page
}
async initialise(code = '') {
const u = await getUtils(this.page)
await this.page.addInitScript(async (code) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
}, code)
await this.page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
}
getInputFile = (fileName: string) => {
return fsp.readFile(
join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName),
'utf-8'
)
}
}
export const test = base.extend<{
app: AuthenticatedApp
cmdBar: CmdBarFixture
editor: EditorFixture
toolbar: ToolbarFixture
scene: SceneFixture
}>({
app: async ({ page }, use) => {
await use(new AuthenticatedApp(page))
},
cmdBar: async ({ page }, use) => {
await use(new CmdBarFixture(page))
},
editor: async ({ page }, use) => {
await use(new EditorFixture(page))
},
toolbar: async ({ page }, use) => {
await use(new ToolbarFixture(page))
},
scene: async ({ page }, use) => {
await use(new SceneFixture(page))
},
})
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
export { expect } from '@playwright/test'

View File

@ -25,11 +25,14 @@ type CmdBarSerialised =
} }
export class CmdBarFixture { export class CmdBarFixture {
public readonly page: Page public page: Page
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
} }
reConstruct = (page: Page) => {
this.page = page
}
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => { private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
const reviewForm = await this.page.locator('#review-form') const reviewForm = await this.page.locator('#review-form')

View File

@ -1,5 +1,6 @@
import type { Page, Locator } from '@playwright/test' import type { Page, Locator } from '@playwright/test'
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { sansWhitespace } from '../test-utils'
interface EditorState { interface EditorState {
activeLines: Array<string> activeLines: Array<string>
@ -7,19 +8,20 @@ interface EditorState {
diagnostics: Array<string> diagnostics: Array<string>
} }
function removeWhitespace(str: string) {
return str.replace(/\s+/g, '').trim()
}
export class EditorFixture { export class EditorFixture {
public readonly page: Page public page: Page
private readonly diagnosticsTooltip: Locator private diagnosticsTooltip!: Locator
private readonly diagnosticsGutterIcon: Locator private diagnosticsGutterIcon!: Locator
private readonly codeContent: Locator private codeContent!: Locator
private readonly activeLine: Locator private activeLine!: Locator
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.codeContent = page.locator('.cm-content') this.codeContent = page.locator('.cm-content')
this.diagnosticsTooltip = page.locator('.cm-tooltip-lint') this.diagnosticsTooltip = page.locator('.cm-tooltip-lint')
@ -94,16 +96,16 @@ export class EditorFixture {
this._serialiseDiagnostics(), this._serialiseDiagnostics(),
]) ])
const state: EditorState = { const state: EditorState = {
activeLines: activeLines.map(removeWhitespace).filter(Boolean), activeLines: activeLines.map(sansWhitespace).filter(Boolean),
highlightedCode: removeWhitespace(highlightedCode), highlightedCode: sansWhitespace(highlightedCode),
diagnostics, diagnostics,
} }
return state return state
}) })
.toEqual({ .toEqual({
activeLines: expectedState.activeLines.map(removeWhitespace), activeLines: expectedState.activeLines.map(sansWhitespace),
highlightedCode: removeWhitespace(expectedState.highlightedCode), highlightedCode: sansWhitespace(expectedState.highlightedCode),
diagnostics: expectedState.diagnostics.map(removeWhitespace), diagnostics: expectedState.diagnostics.map(sansWhitespace),
}) })
} }
} }

View File

@ -0,0 +1,140 @@
import type {
BrowserContext,
ElectronApplication,
Page,
TestInfo,
} from '@playwright/test'
import { test as base } from '@playwright/test'
import { getUtils, setup, setupElectron, tearDown } from '../test-utils'
import fsp from 'fs/promises'
import { join } from 'path'
import { CmdBarFixture } from './cmdBarFixture'
import { EditorFixture } from './editorFixture'
import { ToolbarFixture } from './toolbarFixture'
import { SceneFixture } from './sceneFixture'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { HomePageFixture } from './homePageFixture'
import { unsafeTypedKeys } from 'lib/utils'
export class AuthenticatedApp {
public readonly page: Page
public readonly context: BrowserContext
public readonly testInfo: TestInfo
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this.page = page
this.context = context
this.testInfo = testInfo
}
async initialise(code = '') {
await setup(this.context, this.page, this.testInfo)
const u = await getUtils(this.page)
await this.page.addInitScript(async (code) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
}, code)
await this.page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
}
getInputFile = (fileName: string) => {
return fsp.readFile(
join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName),
'utf-8'
)
}
}
interface Fixtures {
app: AuthenticatedApp
tronApp: AuthenticatedTronApp
cmdBar: CmdBarFixture
editor: EditorFixture
toolbar: ToolbarFixture
scene: SceneFixture
homePage: HomePageFixture
}
export class AuthenticatedTronApp {
public readonly _page: Page
public page: Page
public readonly context: BrowserContext
public readonly testInfo: TestInfo
public electronApp?: ElectronApplication
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this._page = page
this.page = page
this.context = context
this.testInfo = testInfo
}
async initialise(
arg: {
fixtures: Partial<Fixtures>
folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean
appSettings?: Partial<SaveSettingsPayload>
} = { fixtures: {} }
) {
const { electronApp, page } = await setupElectron({
testInfo: this.testInfo,
folderSetupFn: arg.folderSetupFn,
cleanProjectDir: arg.cleanProjectDir,
appSettings: arg.appSettings,
})
this.page = page
this.electronApp = electronApp
await page.setViewportSize({ width: 1200, height: 500 })
for (const key of unsafeTypedKeys(arg.fixtures)) {
const fixture = arg.fixtures[key]
if (
!fixture ||
fixture instanceof AuthenticatedApp ||
fixture instanceof AuthenticatedTronApp
)
continue
fixture.reConstruct(page)
}
}
close = async () => {
await this.electronApp?.close?.()
}
debugPause = () =>
new Promise(() => {
console.log('UN-RESOLVING PROMISE')
})
}
export const test = base.extend<Fixtures>({
app: async ({ page, context }, use, testInfo) => {
await use(new AuthenticatedApp(context, page, testInfo))
},
tronApp: async ({ page, context }, use, testInfo) => {
await use(new AuthenticatedTronApp(context, page, testInfo))
},
cmdBar: async ({ page }, use) => {
await use(new CmdBarFixture(page))
},
editor: async ({ page }, use) => {
await use(new EditorFixture(page))
},
toolbar: async ({ page }, use) => {
await use(new ToolbarFixture(page))
},
scene: async ({ page }, use) => {
await use(new SceneFixture(page))
},
homePage: async ({ page }, use) => {
await use(new HomePageFixture(page))
},
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
export { expect } from '@playwright/test'

View File

@ -0,0 +1,103 @@
import type { Page, Locator } from '@playwright/test'
import { expect } from '@playwright/test'
interface ProjectCardState {
title: string
fileCount: number
folderCount: number
}
interface HomePageState {
projectCards: ProjectCardState[]
sortBy: 'last-modified-desc' | 'last-modified-asc' | 'name-asc' | 'name-desc'
}
export class HomePageFixture {
public page: Page
projectCard!: Locator
projectCardTitle!: Locator
projectCardFile!: Locator
projectCardFolder!: Locator
sortByDateBtn!: Locator
sortByNameBtn!: Locator
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.projectCard = this.page.getByTestId('project-link')
this.projectCardTitle = this.page.getByTestId('project-title')
this.projectCardFile = this.page.getByTestId('project-file-count')
this.projectCardFolder = this.page.getByTestId('project-folder-count')
this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified')
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
}
private _serialiseSortBy = async (): Promise<
HomePageState['sortBy'] | null
> => {
const [dateBtnDesc, dateBtnAsc, nameBtnDesc, nameBtnAsc] =
await Promise.all([
this.sortByDateBtn.getByLabel('arrow down').isVisible(),
this.sortByDateBtn.getByLabel('arrow up').isVisible(),
this.sortByNameBtn.getByLabel('arrow down').isVisible(),
this.sortByNameBtn.getByLabel('arrow up').isVisible(),
])
if (dateBtnDesc) return 'last-modified-desc'
if (dateBtnAsc) return 'last-modified-asc'
if (nameBtnDesc) return 'name-desc'
if (nameBtnAsc) return 'name-asc'
return null
}
private _serialiseProjectCards = async (): Promise<
Array<ProjectCardState>
> => {
const projectCards = await this.projectCard.all()
const projectCardStates: Array<ProjectCardState> = []
for (const projectCard of projectCards) {
const [title, fileCount, folderCount] = await Promise.all([
(await projectCard.locator(this.projectCardTitle).textContent()) || '',
Number(await projectCard.locator(this.projectCardFile).textContent()),
Number(await projectCard.locator(this.projectCardFolder).textContent()),
])
projectCardStates.push({
title: title,
fileCount,
folderCount,
})
}
return projectCardStates
}
/**
* Date is excluded from expectState, since it changes
* Maybe there a good sanity check we can do each time?
*/
expectState = async (expectedState: HomePageState) => {
await expect
.poll(async () => {
const [projectCards, sortBy] = await Promise.all([
this._serialiseProjectCards(),
this._serialiseSortBy(),
])
return {
projectCards,
sortBy,
}
})
.toEqual(expectedState)
}
openProject = async (projectTitle: string) => {
const projectCard = this.projectCard.locator(
this.page.getByText(projectTitle)
)
await projectCard.click()
}
}

View File

@ -6,18 +6,24 @@ import {
doAndWaitForImageDiff, doAndWaitForImageDiff,
openAndClearDebugPanel, openAndClearDebugPanel,
sendCustomCmd, sendCustomCmd,
} from './test-utils' } from '../test-utils'
type mouseParams = { type mouseParams = {
pixelDiff: number pixelDiff: number
} }
export class SceneFixture { export class SceneFixture {
public readonly page: Page public page: Page
private readonly exeIndicator: Locator
private exeIndicator!: Locator
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
} }
@ -25,33 +31,38 @@ export class SceneFixture {
x: number, x: number,
y: number, y: number,
{ steps }: { steps: number } = { steps: 5000 } { steps }: { steps: number } = { steps: 5000 }
) => [ ) =>
(params?: mouseParams) => { [
if (params?.pixelDiff) { (clickParams?: mouseParams) => {
return doAndWaitForImageDiff( if (clickParams?.pixelDiff) {
this.page, return doAndWaitForImageDiff(
() => this.page.mouse.click(x, y), this.page,
params.pixelDiff () => this.page.mouse.click(x, y),
) clickParams.pixelDiff
} )
return this.page.mouse.click(x, y) }
}, return this.page.mouse.click(x, y)
(params?: mouseParams) => { },
if (params?.pixelDiff) { (moveParams?: mouseParams) => {
return doAndWaitForImageDiff( if (moveParams?.pixelDiff) {
this.page, return doAndWaitForImageDiff(
() => this.page.mouse.move(x, y, { steps }), this.page,
params.pixelDiff () => this.page.mouse.move(x, y, { steps }),
) moveParams.pixelDiff
} )
return this.page.mouse.move(x, y, { steps }) }
}, return this.page.mouse.move(x, y, { steps })
] },
] as const
/** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene. /** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene.
* *
* Expects the viewPort to be 1000x500 */ * Expects the viewPort to be 1000x500 */
clickNoWhere = () => this.page.mouse.click(998, 60) clickNoWhere = () => this.page.mouse.click(998, 60)
/** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene.
*
* Expects the viewPort to be 1000x500 */
moveNoWhere = (steps?: number) => this.page.mouse.move(998, 60, { steps })
moveCameraTo = async ( moveCameraTo = async (
pos: { x: number; y: number; z: number }, pos: { x: number; y: number; z: number },

View File

@ -0,0 +1,79 @@
import type { Page, Locator } from '@playwright/test'
import { expect } from './fixtureSetup'
import { doAndWaitForImageDiff } from '../test-utils'
export class ToolbarFixture {
public page: Page
extrudeButton!: Locator
startSketchBtn!: Locator
rectangleBtn!: Locator
exitSketchBtn!: Locator
editSketchBtn!: Locator
fileTreeBtn!: Locator
createFileBtn!: Locator
fileCreateToast!: Locator
filePane!: Locator
exeIndicator!: Locator
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.startSketchBtn = page.getByTestId('sketch')
this.rectangleBtn = page.getByTestId('corner-rectangle')
this.exitSketchBtn = page.getByTestId('sketch-exit')
this.editSketchBtn = page.getByText('Edit Sketch')
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
this.createFileBtn = page.getByTestId('create-file-button')
this.filePane = page.locator('#files-pane')
this.fileCreateToast = page.getByText('Successfully created')
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
}
startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
editSketch = async () => {
await this.editSketchBtn.first().click()
// One of the rare times we want to allow a arbitrary wait
// this is for the engine animation, as it takes 500ms to complete
await this.page.waitForTimeout(600)
}
private _serialiseFileTree = async () => {
return this.page
.locator('#files-pane')
.getByTestId('file-tree-item')
.allInnerTexts()
}
/**
* TODO folders, in expect state
*/
expectFileTreeState = async (expected: string[]) => {
await expect.poll(this._serialiseFileTree).toEqual(expected)
}
createFile = async ({ wait }: { wait: boolean } = { wait: false }) => {
await this.createFileBtn.click()
await expect(this.fileCreateToast).toBeVisible()
if (wait) {
await this.fileCreateToast.waitFor({ state: 'detached' })
}
}
/**
* Opens file by it's name and waits for execution to finish
*/
openFile = async (
fileName: string,
{ wait }: { wait?: boolean } = { wait: true }
) => {
await this.filePane.getByText(fileName).click()
if (wait) {
await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
}
}
}

View File

@ -0,0 +1,7 @@
import { test, expect } from '@playwright/test'
/** @deprecated, import from ./fixtureSetup.ts instead */
export const _test = test
/** @deprecated, import from ./fixtureSetup.ts instead */
export const _expect = expect

View File

@ -1,7 +1,7 @@
import { test, expect, AuthenticatedApp } from './fixtureSetup' import { test, expect, AuthenticatedApp } from './fixtures/fixtureSetup'
import { EditorFixture } from './editorFixture' import { EditorFixture } from './fixtures/editorFixture'
import { SceneFixture } from './sceneFixture' import { SceneFixture } from './fixtures/sceneFixture'
import { ToolbarFixture } from './toolbarFixture' import { ToolbarFixture } from './fixtures/toolbarFixture'
// test file is for testing point an click code gen functionality that's not sketch mode related // test file is for testing point an click code gen functionality that's not sketch mode related

View File

@ -474,6 +474,7 @@ test(
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
}) })
} }
) )
@ -531,6 +532,7 @@ test(
// Ensure the draft rectangle looks the same as it usually does // Ensure the draft rectangle looks the same as it usually does
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
}) })
} }
) )
@ -585,6 +587,7 @@ test(
// Ensure the draft rectangle looks the same as it usually does // Ensure the draft rectangle looks the same as it usually does
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
}) })
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ') `const sketch001 = startSketchOn('XZ')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -1066,3 +1066,7 @@ export async function openAndClearDebugPanel(page: Page) {
await openDebugPanel(page) await openDebugPanel(page)
return clearCommandLogs(page) return clearCommandLogs(page)
} }
export function sansWhitespace(str: string) {
return str.replace(/\s+/g, '').trim()
}

View File

@ -1,21 +0,0 @@
import type { Page, Locator } from '@playwright/test'
import { doAndWaitForImageDiff } from './test-utils'
export class ToolbarFixture {
public readonly page: Page
readonly extrudeButton: Locator
readonly startSketchBtn: Locator
readonly rectangleBtn: Locator
readonly exitSketchBtn: Locator
constructor(page: Page) {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.startSketchBtn = page.getByTestId('sketch')
this.rectangleBtn = page.getByTestId('corner-rectangle')
this.exitSketchBtn = page.getByTestId('sketch-exit')
}
startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
}

View File

@ -194,7 +194,7 @@ const FileTreeItem = ({
} }
return ( return (
<div className="contents" ref={itemRef}> <div className="contents" data-testid="file-tree-item" ref={itemRef}>
{fileOrDir.children === null ? ( {fileOrDir.children === null ? (
<li <li
className={ className={
@ -389,12 +389,14 @@ interface FileTreeProps {
export const FileTreeMenu = () => { export const FileTreeMenu = () => {
const { send } = useFileContext() const { send } = useFileContext()
const { send: modelingSend } = useModelingContext()
function createFile() { function createFile() {
send({ send({
type: 'Create file', type: 'Create file',
data: { name: '', makeDir: false, shouldSetToRename: true }, data: { name: '', makeDir: false, shouldSetToRename: true },
}) })
modelingSend({ type: 'Cancel' })
} }
function createFolder() { function createFolder() {

View File

@ -104,20 +104,33 @@ function ProjectCard({
ref={inputRef} ref={inputRef}
/> />
) : ( ) : (
<h3 className="font-sans relative z-0 p-2"> <h3
className="font-sans relative z-0 p-2"
data-testid="project-title"
>
{project.name?.replace(FILE_EXT, '')} {project.name?.replace(FILE_EXT, '')}
</h3> </h3>
)} )}
<span className="px-2 text-chalkboard-60 text-xs"> <span className="px-2 text-chalkboard-60 text-xs">
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '} <span data-testid="project-file-count">{numberOfFiles}</span> file
{numberOfFolders > 0 && {numberOfFiles === 1 ? '' : 's'}{' '}
`/ ${numberOfFolders} folder${numberOfFolders === 1 ? '' : 's'}`} {numberOfFolders > 0 && (
<>
{'/ '}
<span data-testid="project-folder-count">
{numberOfFolders}
</span>{' '}
folder{numberOfFolders === 1 ? '' : 's'}
</>
)}
</span> </span>
<span className="px-2 text-chalkboard-60 text-xs"> <span className="px-2 text-chalkboard-60 text-xs">
Edited{' '} Edited{' '}
{project.metadata && project.metadata.modified <span data-testid="project-edit-date">
? getDisplayedTime(parseInt(project.metadata.modified)) {project.metadata && project.metadata.modified
: 'never'} ? getDisplayedTime(parseInt(project.metadata.modified))
: 'never'}
</span>
</span> </span>
</div> </div>
</Link> </Link>

View File

@ -15,6 +15,30 @@ export function isArray(val: any): val is unknown[] {
} }
/** /**
* An alternative to `Object.keys()` that returns an array of keys with types.
*
* It's UNSAFE because because of TS's structural subtyping and how at runtime, you can
* extend a JS object with whatever keys you want.
*
* Why we shouldn't be extending objects with arbitrary keys at run time, the structural subtyping
* issue could be a confusing bug, for example, in the below snippet `myKeys` is typed as
* `('x' | 'y')[]` but is really `('x' | 'y' | 'name')[]`
* ```ts
* interface Point { x: number; y: number }
* interface NamedPoint { x: number; y: number; name: string }
*
* let point: Point = { x: 1, y: 2 }
* let namedPoint: NamedPoint = { x: 1, y: 2, name: 'A' }
*
* // Structural subtyping allows this assignment
* point = namedPoint // This is allowed because NamedPoint has all properties of Point
* const myKeys = unsafeTypedKeys(point) // typed as ('x' | 'y')[] but is really ('x' | 'y' | 'name')[]
* ```
*/
export function unsafeTypedKeys<T extends object>(obj: T): Array<keyof T> {
return Object.keys(obj) as Array<keyof T>
}
/*
* Predicate that checks if a value is not null and not undefined. This is * Predicate that checks if a value is not null and not undefined. This is
* useful for functions like Array::filter() and Array::find() that have * useful for functions like Array::filter() and Array::find() that have
* overloads that accept a type guard. * overloads that accept a type guard.

View File

@ -248,6 +248,7 @@ const Home = () => {
<small>Sort by</small> <small>Sort by</small>
<ActionButton <ActionButton
Element="button" Element="button"
data-testid="home-sort-by-name"
className={ className={
'text-xs border-primary/10 ' + 'text-xs border-primary/10 ' +
(!sort.includes('name') (!sort.includes('name')
@ -269,6 +270,7 @@ const Home = () => {
</ActionButton> </ActionButton>
<ActionButton <ActionButton
Element="button" Element="button"
data-testid="home-sort-by-modified"
className={ className={
'text-xs border-primary/10 ' + 'text-xs border-primary/10 ' +
(!isSortByModified (!isSortByModified