Compare commits
	
		
			13 Commits
		
	
	
		
			kurt-test-
			...
			ok
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6af475cf91 | |||
| 2b5fd32699 | |||
| 743aa606f9 | |||
| 9a4e061be3 | |||
| 42028798e4 | |||
| 319251f98c | |||
| c545834a52 | |||
| bc8cf38936 | |||
| 535911fc0a | |||
| 19575a16d7 | |||
| 2ae92d5341 | |||
| fca1f02d88 | |||
| f752d47dd2 | 
							
								
								
									
										204
									
								
								e2e/playwright/file-tree.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								e2e/playwright/file-tree.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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() | ||||
|     } | ||||
|   ) | ||||
| }) | ||||
| @ -532,18 +532,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( | ||||
|  | ||||
| @ -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 ( | ||||
|     <button | ||||
|       data-testid={props['data-testid']} | ||||
|       className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left" | ||||
|       onClick={onClick} | ||||
|     > | ||||
|  | ||||
| @ -16,7 +16,11 @@ import { | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { fileMachine } from 'machines/fileMachine' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
| import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants' | ||||
| import { | ||||
|   DEFAULT_FILE_NAME, | ||||
|   DEFAULT_PROJECT_KCL_FILE, | ||||
|   FILE_EXT, | ||||
| } from 'lib/constants' | ||||
| import { getProjectInfo } from 'lib/desktop' | ||||
| import { getNextDirName, getNextFileName } from 'lib/desktopFS' | ||||
|  | ||||
| @ -167,6 +171,25 @@ export const FileMachineProvider = ({ | ||||
|           name | ||||
|         ) | ||||
|  | ||||
|         // no-op | ||||
|         if (oldPath === newPath) { | ||||
|           return { | ||||
|             message: `Old is the same as new.`, | ||||
|             newPath, | ||||
|             oldPath, | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // if there are any siblings with the same name, report error. | ||||
|         const entries = await window.electron.readdir( | ||||
|           window.electron.path.dirname(newPath) | ||||
|         ) | ||||
|         for (let entry of entries) { | ||||
|           if (entry === newName) { | ||||
|             return Promise.reject(new Error('Filename already exists.')) | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         window.electron.rename(oldPath, newPath) | ||||
|  | ||||
|         if (!file) { | ||||
| @ -209,6 +232,22 @@ export const FileMachineProvider = ({ | ||||
|             .catch((e) => console.error('Error deleting file', e)) | ||||
|         } | ||||
|  | ||||
|         // If there are no more files at all in the project, create a main.kcl | ||||
|         // for when we navigate to the root. | ||||
|         if (!project?.path) { | ||||
|           return Promise.reject(new Error('Project path not set.')) | ||||
|         } | ||||
|  | ||||
|         const entries = await window.electron.readdir(project.path) | ||||
|         const hasKclEntries = | ||||
|           entries.filter((e: string) => e.endsWith('.kcl')).length !== 0 | ||||
|         if (!hasKclEntries) { | ||||
|           await window.electron.writeFile( | ||||
|             window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE), | ||||
|             '' | ||||
|           ) | ||||
|         } | ||||
|  | ||||
|         // If we just deleted the current file or one of its parent directories, | ||||
|         // navigate to the project root | ||||
|         if ( | ||||
| @ -219,6 +258,11 @@ export const FileMachineProvider = ({ | ||||
|           navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`) | ||||
|         } | ||||
|  | ||||
|         // Refresh the route selected above because it's possible we're on | ||||
|         // the same path on the navigate, which doesn't cause anything to | ||||
|         // refresh, leaving a stale execution state. | ||||
|         navigate(0) | ||||
|  | ||||
|         return `Successfully deleted ${isDir ? 'folder' : 'file'} "${ | ||||
|           event.data.name | ||||
|         }"` | ||||
|  | ||||
| @ -358,10 +358,18 @@ function FileTreeContextMenu({ | ||||
|     <ContextMenu | ||||
|       menuTargetElement={itemRef} | ||||
|       items={[ | ||||
|         <ContextMenuItem onClick={onRename} hotkey="Enter"> | ||||
|         <ContextMenuItem | ||||
|           data-testid="context-menu-rename" | ||||
|           onClick={onRename} | ||||
|           hotkey="Enter" | ||||
|         > | ||||
|           Rename | ||||
|         </ContextMenuItem>, | ||||
|         <ContextMenuItem onClick={onDelete} hotkey={metaKey + ' + Del'}> | ||||
|         <ContextMenuItem | ||||
|           data-testid="context-menu-delete" | ||||
|           onClick={onDelete} | ||||
|           hotkey={metaKey + ' + Del'} | ||||
|         > | ||||
|           Delete | ||||
|         </ContextMenuItem>, | ||||
|       ]} | ||||
|  | ||||
| @ -8,6 +8,7 @@ export const MAX_PADDING = 7 | ||||
|  * This is available for users to edit as a setting. | ||||
|  */ | ||||
| export const DEFAULT_PROJECT_NAME = 'project-$nnn' | ||||
| export const DEFAULT_PROJECT_KCL_FILE = 'main.kcl' | ||||
| /** Name given the temporary "project" in the browser version of the app */ | ||||
| export const BROWSER_PROJECT_NAME = 'browser' | ||||
| /** Name given the temporary file in the browser version of the app */ | ||||
|  | ||||
| @ -90,12 +90,22 @@ export const fileLoader: LoaderFunction = async ( | ||||
|     let code = '' | ||||
|  | ||||
|     if (!urlObj.pathname.endsWith('/settings')) { | ||||
|       if (!currentFileName || !currentFilePath || !projectName) { | ||||
|       const fallbackFile = (await getProjectInfo(projectPath)).default_file | ||||
|       let fileExists = true | ||||
|       if (currentFilePath) { | ||||
|         try { | ||||
|           await window.electron.stat(currentFilePath) | ||||
|         } catch (e) { | ||||
|           if (e === 'ENOENT') { | ||||
|             fileExists = false | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (!fileExists || !currentFileName || !currentFilePath || !projectName) { | ||||
|         return redirect( | ||||
|           `${PATHS.FILE}/${encodeURIComponent( | ||||
|             isDesktop() | ||||
|               ? (await getProjectInfo(projectPath)).default_file | ||||
|               : params.id + '/' + PROJECT_ENTRYPOINT | ||||
|             isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT | ||||
|           )}` | ||||
|         ) | ||||
|       } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	