diff --git a/e2e/playwright/file-tree.spec.ts b/e2e/playwright/file-tree.spec.ts new file mode 100644 index 000000000..c6eb0e6b7 --- /dev/null +++ b/e2e/playwright/file-tree.spec.ts @@ -0,0 +1,204 @@ +import { test, expect } from '@playwright/test' +import * as fsp from 'fs/promises' +import { getUtils, setup, setupElectron, tearDown } from './test-utils' + +test.beforeEach(async ({ context, page }) => { + await setup(context, page) +}) + +test.afterEach(async ({ page }, testInfo) => { + await tearDown(page, testInfo) +}) + +test.describe('when using the file tree to', () => { + const fromFile = 'main.kcl' + const toFile = 'hello.kcl' + + test( + `rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`, + { tag: '@electron' }, + async ({ browser: _ }, testInfo) => { + const { electronApp, page } = await setupElectron({ + testInfo, + folderSetupFn: async () => {}, + }) + + const { + panesOpen, + createAndSelectProject, + pasteCodeInEditor, + renameFile, + editorTextMatches, + } = await getUtils(page, test) + + await page.setViewportSize({ width: 1200, height: 500 }) + page.on('console', console.log) + + await panesOpen(['files', 'code']) + + await createAndSelectProject('project-000') + + // File the main.kcl with contents + const kclCube = await fsp.readFile( + 'src/wasm-lib/tests/executor/inputs/cube.kcl', + 'utf-8' + ) + await pasteCodeInEditor(kclCube) + + await renameFile(fromFile, toFile) + await page.reload() + + await test.step('Postcondition: editor has same content as before the rename', async () => { + await editorTextMatches(kclCube) + }) + + await test.step('Postcondition: opening and closing settings works', async () => { + const settingsOpenButton = page.getByRole('link', { + name: 'settings Settings', + }) + const settingsCloseButton = page.getByTestId('settings-close-button') + await settingsOpenButton.click() + await settingsCloseButton.click() + }) + + await electronApp.close() + } + ) + + test( + `create many new untitled files they increment their names`, + { tag: '@electron' }, + async ({ browser: _ }, testInfo) => { + const { electronApp, page } = await setupElectron({ + testInfo, + folderSetupFn: async () => {}, + }) + + const { panesOpen, createAndSelectProject, createNewFile } = + await getUtils(page, test) + + await page.setViewportSize({ width: 1200, height: 500 }) + page.on('console', console.log) + + await panesOpen(['files']) + + await createAndSelectProject('project-000') + + await createNewFile('') + await createNewFile('') + await createNewFile('') + await createNewFile('') + await createNewFile('') + + await test.step('Postcondition: there are 5 new Untitled-*.kcl files', async () => { + await expect( + page + .locator('[data-testid="file-pane-scroll-container"] button') + .filter({ hasText: /Untitled[-]?[0-5]?/ }) + ).toHaveCount(5) + }) + + await electronApp.close() + } + ) + + test( + 'create a new file with the same name as an existing file cancels the operation', + { tag: '@electron' }, + async ({ browser: _ }, testInfo) => { + const { electronApp, page } = await setupElectron({ + testInfo, + folderSetupFn: async () => {}, + }) + + const { + panesOpen, + createAndSelectProject, + pasteCodeInEditor, + createNewFileAndSelect, + renameFile, + selectFile, + editorTextMatches, + } = await getUtils(page, test) + + await page.setViewportSize({ width: 1200, height: 500 }) + page.on('console', console.log) + + await panesOpen(['files', 'code']) + + await createAndSelectProject('project-000') + // File the main.kcl with contents + const kclCube = await fsp.readFile( + 'src/wasm-lib/tests/executor/inputs/cube.kcl', + 'utf-8' + ) + await pasteCodeInEditor(kclCube) + + const kcl1 = 'main.kcl' + const kcl2 = '2.kcl' + + await createNewFileAndSelect(kcl2) + const kclCylinder = await fsp.readFile( + 'src/wasm-lib/tests/executor/inputs/cylinder.kcl', + 'utf-8' + ) + await pasteCodeInEditor(kclCylinder) + + await renameFile(kcl2, kcl1) + + await test.step(`Postcondition: ${kcl1} still has the original content`, async () => { + await selectFile(kcl1) + await editorTextMatches(kclCube) + }) + + await test.step(`Postcondition: ${kcl2} still exists with the original content`, async () => { + await selectFile(kcl2) + await editorTextMatches(kclCylinder) + }) + + await electronApp.close() + } + ) + + test( + 'deleting all files recreates a default main.kcl with no code', + { tag: '@electron' }, + async ({ browser: _ }, testInfo) => { + const { electronApp, page } = await setupElectron({ + testInfo, + folderSetupFn: async () => {}, + }) + + const { + panesOpen, + createAndSelectProject, + pasteCodeInEditor, + deleteFile, + editorTextMatches, + } = await getUtils(page, test) + + await page.setViewportSize({ width: 1200, height: 500 }) + page.on('console', console.log) + + await panesOpen(['files', 'code']) + + await createAndSelectProject('project-000') + // File the main.kcl with contents + const kclCube = await fsp.readFile( + 'src/wasm-lib/tests/executor/inputs/cube.kcl', + 'utf-8' + ) + await pasteCodeInEditor(kclCube) + + const kcl1 = 'main.kcl' + + await deleteFile(kcl1) + + await test.step(`Postcondition: ${kcl1} is recreated but has no content`, async () => { + await editorTextMatches('') + }) + + await electronApp.close() + } + ) +}) diff --git a/e2e/playwright/test-utils.ts b/e2e/playwright/test-utils.ts index 9d8991feb..6f43ad933 100644 --- a/e2e/playwright/test-utils.ts +++ b/e2e/playwright/test-utils.ts @@ -511,10 +511,7 @@ export async function getUtils(page: Page, test_?: typeof test) { editorTextMatches: async (code: string) => { const editor = page.locator(editorSelector) - const editorText = await editor.textContent() - return expect(util.toNormalizedCode(editorText || '')).toBe( - util.toNormalizedCode(code) - ) + return expect(editor).toHaveText(code, { useInnerText: true }) }, pasteCodeInEditor: async (code: string) => { @@ -532,18 +529,62 @@ export async function getUtils(page: Page, test_?: typeof test) { }) }, + createNewFile: async (name: string) => { + return test?.step(`Create a file named ${name}`, async () => { + await page.getByTestId('create-file-button').click() + await page.getByTestId('file-rename-field').fill(name) + await page.keyboard.press('Enter') + }) + }, + + selectFile: async (name: string) => { + return test?.step(`Select ${name}`, async () => { + await page + .locator('[data-testid="file-pane-scroll-container"] button') + .filter({ hasText: name }) + .click() + }) + }, + createNewFileAndSelect: async (name: string) => { return test?.step(`Create a file named ${name}, select it`, async () => { await page.getByTestId('create-file-button').click() await page.getByTestId('file-rename-field').fill(name) await page.keyboard.press('Enter') await page - .getByTestId('file-pane-scroll-container') + .locator('[data-testid="file-pane-scroll-container"] button') .filter({ hasText: name }) .click() }) }, + renameFile: async (fromName: string, toName: string) => { + return test?.step(`Rename ${fromName} to ${toName}`, async () => { + await page + .locator('[data-testid="file-pane-scroll-container"] button') + .filter({ hasText: fromName }) + .click({ button: 'right' }) + await page.getByTestId('context-menu-rename').click() + await page.getByTestId('file-rename-field').fill(toName) + await page.keyboard.press('Enter') + await page + .locator('[data-testid="file-pane-scroll-container"] button') + .filter({ hasText: toName }) + .click() + }) + }, + + deleteFile: async (name: string) => { + return test?.step(`Delete ${name}`, async () => { + await page + .locator('[data-testid="file-pane-scroll-container"] button') + .filter({ hasText: name }) + .click({ button: 'right' }) + await page.getByTestId('context-menu-delete').click() + await page.getByTestId('delete-confirmation').click() + }) + }, + panesOpen: async (paneIds: PaneId[]) => { return test?.step(`Setting ${paneIds} panes to be open`, async () => { await page.addInitScript( diff --git a/e2e/playwright/text-to-cad-tests.spec.ts b/e2e/playwright/text-to-cad-tests.spec.ts index 3a22036a9..8dc2b6d7b 100644 --- a/e2e/playwright/text-to-cad-tests.spec.ts +++ b/e2e/playwright/text-to-cad-tests.spec.ts @@ -1,11 +1,5 @@ import { test, expect, Page } from '@playwright/test' -import { - getUtils, - setup, - tearDown, - setupElectron, - createProjectAndRenameIt, -} from './test-utils' +import { getUtils, setup, tearDown, setupElectron } from './test-utils' import { join } from 'path' import fs from 'fs' @@ -698,13 +692,16 @@ test( async ({ browserName }, testInfo) => { const { electronApp, page, dir } = await setupElectron({ testInfo }) const fileExists = () => - fs.existsSync(join(dir, 'test-000', 'lego-2x4.kcl')) + fs.existsSync(join(dir, 'project-000', 'lego-2x4.kcl')) + + const { createAndSelectProject, panesOpen } = await getUtils(page, test) await page.setViewportSize({ width: 1200, height: 500 }) + await panesOpen(['code', 'files']) + // Create and navigate to the project - await createProjectAndRenameIt({ name: 'test-000', page }) - await page.getByTestId('project-link').click() + await createAndSelectProject('project-000') // Wait for Start Sketch otherwise you will not have access Text-to-CAD command await expect( @@ -713,10 +710,6 @@ test( timeout: 20_000, }) - // Open the files pane - const filesPaneButton = page.getByTestId('files-pane-button') - await filesPaneButton.click() - await test.step(`Test file creation`, async () => { await sendPromptFromCommandBar(page, 'lego 2x4') // File is considered created if it shows up in the Project Files pane diff --git a/playwright-electron.config.ts b/playwright-electron.config.ts deleted file mode 100644 index 8eeaed1af..000000000 --- a/playwright-electron.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { defineConfig, devices } from '@playwright/test' -import dotenv from 'dotenv' - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - timeout: 120_000, // override the default 30s timeout - testDir: './e2e/playwright', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Do not retry */ - retries: process.env.CI ? 0 : 0, - /* Different amount of parallelism on CI and local. */ - workers: process.env.CI ? 1 : 4, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [ - [process.env.CI ? 'dot' : 'list'], - ['json', { outputFile: './test-results/report.json' }], - ['html'], - ], - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'retain-on-failure', - actionTimeout: 15000, - screenshot: 'only-on-failure', - }, -}) diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index b57c6aa7f..05722cca4 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -135,16 +135,15 @@ interface ContextMenuItemProps { icon?: ActionIconProps['icon'] onClick?: () => void hotkey?: string + 'data-testid'?: string } -export function ContextMenuItem({ - children, - icon, - onClick, - hotkey, -}: ContextMenuItemProps) { +export function ContextMenuItem(props: ContextMenuItemProps) { + const { children, icon, onClick, hotkey } = props + return (