diff --git a/rtl-spec/components/sidebar-file-tree.spec.tsx b/rtl-spec/components/sidebar-file-tree.spec.tsx index 922df2eb12..5b5cb07686 100644 --- a/rtl-spec/components/sidebar-file-tree.spec.tsx +++ b/rtl-spec/components/sidebar-file-tree.spec.tsx @@ -96,7 +96,35 @@ describe('SidebarFileTree component', () => { ) as HTMLInputElement; await user.type(input, 'tester.js{Enter}'); - expect(editorMosaic.files.get('tester.js')).toBe(EditorPresence.Pending); + expect(editorMosaic.files.get('tester.js')).toBe(EditorPresence.Hidden); + }); + + it('fails to create new editors (file name with path separator)', async () => { + const user = userEvent.setup(); + const { container } = render(); + const EDITOR_NEW_NAME = 'subdir/tester.js'; + + expect(editorMosaic.files.get(EDITOR_NEW_NAME)).toBe(undefined); + + // Click the "Add New File" button (by icon) + const addButton = container.querySelector( + 'button .bp3-icon-add', + )?.parentElement; + await user.click(addButton!); + + // Type the filename and press Enter + const input = container.querySelector( + '#new-file-input', + ) as HTMLInputElement; + await user.type(input, `${EDITOR_NEW_NAME}{Enter}`); + + // Wait for error dialog to be called + await waitFor(() => { + console.log('store.showErrorDialog: ', store.showErrorDialog); + expect(store.showErrorDialog).toHaveBeenCalledWith( + `Invalid filename "${EDITOR_NEW_NAME}": filenames cannot include path separators`, + ); + }); }); it('can delete editors', async () => { @@ -237,6 +265,36 @@ describe('SidebarFileTree component', () => { expect(editorMosaic.files.get(EDITOR_NAME)).toBe(EditorPresence.Pending); }); + it('fails if trying to rename an editor to an unsupported name (file name with path separator)', async () => { + const user = userEvent.setup(); + const EDITOR_NAME = 'index.html'; + const EDITOR_NEW_NAME = 'msg/data.json'; + + store.showInputDialog = vi.fn().mockResolvedValueOnce(EDITOR_NEW_NAME); + store.showErrorDialog = vi.fn().mockResolvedValueOnce(true); + + const { container } = render(); + + // Right-click on index.html to open context menu + const fileLabel = Array.from(container.querySelectorAll('.pointer')).find( + (el) => el.textContent === EDITOR_NAME, + ) as HTMLElement; + fireEvent.contextMenu(fileLabel); + + // Click the "Rename" menu item + const renameItem = await screen.findByText('Rename'); + await user.click(renameItem); + + // Wait for error dialog to be called + await waitFor(() => { + expect(store.showErrorDialog).toHaveBeenCalledWith( + `Invalid filename "${EDITOR_NEW_NAME}": filenames cannot include path separators`, + ); + }); + + expect(editorMosaic.files.get(EDITOR_NAME)).toBe(EditorPresence.Pending); + }); + it('fails if trying to rename an editor to an existing name', async () => { const user = userEvent.setup(); const EXISTED_NAME = 'styles.css'; diff --git a/src/renderer/components/sidebar-file-tree.tsx b/src/renderer/components/sidebar-file-tree.tsx index 634b055920..d7148541e1 100644 --- a/src/renderer/components/sidebar-file-tree.tsx +++ b/src/renderer/components/sidebar-file-tree.tsx @@ -14,10 +14,10 @@ import { ContextMenu2, Tooltip2 } from '@blueprintjs/popover2'; import classNames from 'classnames'; import { observer } from 'mobx-react'; -import { EditorId, PACKAGE_NAME } from '../../interfaces'; +import { EditorId } from '../../interfaces'; import { EditorPresence } from '../editor-mosaic'; import { AppState } from '../state'; -import { isMainEntryPoint, isSupportedFile } from '../utils/editor-utils'; +import { isMainEntryPoint } from '../utils/editor-utils'; interface FileTreeProps { appState: AppState; @@ -108,7 +108,7 @@ export const SidebarFileTree = observer( { + onKeyDown={async (e) => { if (e.key === 'Escape') { e.currentTarget.blur(); } else if (e.key === 'Enter') { @@ -189,29 +189,13 @@ export const SidebarFileTree = observer( if (!id) return; - if ( - id.endsWith('.json') && - [PACKAGE_NAME, 'package-lock.json'].includes(id) - ) { - await appState.showErrorDialog( - `Cannot add ${PACKAGE_NAME} or package-lock.json as custom files`, - ); - return; - } - - if (!isSupportedFile(id)) { - await appState.showErrorDialog( - `Invalid filename "${id}": Must be a file ending in .cjs, .js, .mjs, .html, .css, or .json`, - ); - return; - } - try { await appState.editorMosaic.renameFile(editorId, id); if (visible) appState.editorMosaic.show(id); } catch (err: any) { - appState.showErrorDialog(err.message); + await new Promise((r) => setTimeout(() => r(), 100)); + await appState.showErrorDialog(err.message); } }; @@ -220,13 +204,14 @@ export const SidebarFileTree = observer( editorMosaic.remove(editorId); }; - public createEditor = (editorId: EditorId) => { + public createEditor = async (editorId: EditorId) => { const { appState } = this.props; try { - appState.editorMosaic.addNewFile(editorId); - appState.editorMosaic.show(editorId); + await appState.editorMosaic.addNewFile(editorId); + await appState.editorMosaic.show(editorId); } catch (err: any) { - appState.showErrorDialog(err.message); + await new Promise((r) => setTimeout(() => r(), 100)); + await appState.showErrorDialog(err.message); } }; diff --git a/src/renderer/editor-mosaic.ts b/src/renderer/editor-mosaic.ts index 325196fd52..df4181d223 100644 --- a/src/renderer/editor-mosaic.ts +++ b/src/renderer/editor-mosaic.ts @@ -287,6 +287,18 @@ export class EditorMosaic { throw new Error(`Cannot add file "${id}": File already exists`); } + if (id.includes('/') || id.includes('\\')) { + throw new Error( + `Invalid filename "${id}": filenames cannot include path separators`, + ); + } + + if (!isSupportedFile(id)) { + throw new Error( + `Invalid filename "${id}": Must be a file ending in .cjs, .js, .mjs, .html, .css, or .json`, + ); + } + const entryPoint = this.mainEntryPointFile(); if (isMainEntryPoint(id) && entryPoint) { @@ -308,6 +320,27 @@ export class EditorMosaic { throw new Error(`Cannot rename file to "${newId}": File already exists`); } + if (newId.includes('/') || newId.includes('\\')) { + throw new Error( + `Invalid filename "${newId}": filenames cannot include path separators`, + ); + } + + if ( + newId.endsWith('.json') && + [PACKAGE_NAME, 'package-lock.json'].includes(newId) + ) { + throw new Error( + `Cannot add ${PACKAGE_NAME} or package-lock.json as custom files`, + ); + } + + if (!isSupportedFile(newId)) { + throw new Error( + `Invalid filename "${newId}": Must be a file ending in .cjs, .js, .mjs, .html, .css, or .json`, + ); + } + const entryPoint = this.mainEntryPointFile(); if (isMainEntryPoint(newId) && entryPoint !== oldId) { diff --git a/src/renderer/state.ts b/src/renderer/state.ts index c421af435f..a2ee654b42 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -1023,14 +1023,16 @@ export class AppState { }); } - public async showErrorDialog(label: string | JSX.Element): Promise { + public showErrorDialog = async ( + label: string | JSX.Element, + ): Promise => { await this.showGenericDialog({ label, ok: 'Close', type: GenericDialogType.warning, wantsInput: false, }); - } + }; /** * Ensure that any buffered console output is