From 9073be24b3add17c9a3713b1e919054a15b0de6b Mon Sep 17 00:00:00 2001 From: Rahul Gavande Date: Tue, 30 Dec 2025 15:33:28 +0530 Subject: [PATCH 1/4] Fix renderer crash for long site names when creating new site - Add error handling in handleSiteNameChange to catch ENAMETOOLONG errors - Display user-friendly error message when site name is too long - Add tests to verify ENAMETOOLONG error handling Fixes STU-1182 --- src/hooks/tests/use-add-site.test.tsx | 66 ++++++++++++++++++++++++--- src/hooks/use-add-site.ts | 53 ++++++++++++--------- 2 files changed, 91 insertions(+), 28 deletions(-) diff --git a/src/hooks/tests/use-add-site.test.tsx b/src/hooks/tests/use-add-site.test.tsx index c56433daf6..9f047267ab 100644 --- a/src/hooks/tests/use-add-site.test.tsx +++ b/src/hooks/tests/use-add-site.test.tsx @@ -23,18 +23,23 @@ jest.mock( 'src/hooks/use-import-export', () => ( { } ) ); const mockConnectWpcomSites = jest.fn().mockResolvedValue( undefined ); +const mockGenerateProposedSitePath = jest.fn().mockResolvedValue( { + path: '/default/path', + name: 'Default Site', + isEmpty: true, + isWordPress: false, +} ); + +const mockComparePaths = jest.fn().mockResolvedValue( false ); + jest.mock( 'src/lib/get-ipc-api', () => ( { getIpcApi: () => ( { - generateProposedSitePath: jest.fn().mockResolvedValue( { - path: '/default/path', - name: 'Default Site', - isEmpty: true, - isWordPress: false, - } ), + generateProposedSitePath: mockGenerateProposedSitePath, showNotification: jest.fn(), getAllCustomDomains: jest.fn().mockResolvedValue( [] ), connectWpcomSites: mockConnectWpcomSites, getConnectedWpcomSites: jest.fn().mockResolvedValue( [] ), + comparePaths: mockComparePaths, } ), } ) ); @@ -246,4 +251,53 @@ describe( 'useAddSite', () => { } ); expect( mockSetSelectedTab ).toHaveBeenCalledWith( 'sync' ); } ); + + describe( 'handleSiteNameChange', () => { + beforeEach( () => { + mockGenerateProposedSitePath.mockReset(); + mockGenerateProposedSitePath.mockResolvedValue( { + path: '/default/path', + name: 'Default Site', + isEmpty: true, + isWordPress: false, + } ); + mockComparePaths.mockReset(); + mockComparePaths.mockResolvedValue( false ); + } ); + + it( 'should set user-friendly error when site name causes ENAMETOOLONG error', async () => { + const enametoolongError = new Error( + "Error invoking remote method 'generateProposedSitePath': Error: ENAMETOOLONG: name too long, stat" + ); + mockGenerateProposedSitePath.mockRejectedValueOnce( enametoolongError ); + + const { result } = renderHookWithProvider( () => useAddSite() ); + + await act( async () => { + await result.current.handleSiteNameChange( 'a'.repeat( 300 ) ); + } ); + + expect( result.current.error ).toBe( + 'The site name is too long. Please choose a shorter site name.' + ); + } ); + + it( 'should successfully update site name when path is valid', async () => { + mockGenerateProposedSitePath.mockResolvedValueOnce( { + path: '/default/path/my-site', + name: 'my-site', + isEmpty: true, + isWordPress: false, + } ); + + const { result } = renderHookWithProvider( () => useAddSite() ); + + await act( async () => { + await result.current.handleSiteNameChange( 'my-site' ); + } ); + + expect( result.current.siteName ).toBe( 'my-site' ); + expect( result.current.error ).toBe( '' ); + } ); + } ); } ); diff --git a/src/hooks/use-add-site.ts b/src/hooks/use-add-site.ts index 53afe155ed..57c6053587 100644 --- a/src/hooks/use-add-site.ts +++ b/src/hooks/use-add-site.ts @@ -244,30 +244,39 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { return; } setError( '' ); - const { - path: proposedPath, - isEmpty, - isWordPress, - } = await getIpcApi().generateProposedSitePath( name ); - setProposedSitePath( proposedPath ); - if ( await siteWithPathAlreadyExists( proposedPath ) ) { - setError( - __( - 'The directory is already associated with another Studio site. Please choose a different site name or a custom local path.' - ) - ); - return; - } - if ( ! isEmpty && ! isWordPress ) { - setError( - __( - 'This directory is not empty. Please select an empty directory or an existing WordPress folder.' - ) - ); - return; + try { + const { + path: proposedPath, + isEmpty, + isWordPress, + } = await getIpcApi().generateProposedSitePath( name ); + setProposedSitePath( proposedPath ); + + if ( await siteWithPathAlreadyExists( proposedPath ) ) { + setError( + __( + 'The directory is already associated with another Studio site. Please choose a different site name or a custom local path.' + ) + ); + return; + } + if ( ! isEmpty && ! isWordPress ) { + setError( + __( + 'This directory is not empty. Please select an empty directory or an existing WordPress folder.' + ) + ); + return; + } + setDoesPathContainWordPress( ! isEmpty && isWordPress ); + } catch ( err ) { + if ( err instanceof Error && err.message.includes( 'ENAMETOOLONG' ) ) { + setError( __( 'The site name is too long. Please choose a shorter site name.' ) ); + return; + } + throw err; } - setDoesPathContainWordPress( ! isEmpty && isWordPress ); }, [ __, sitePath, siteWithPathAlreadyExists ] ); From 6931b1fb807d9e414814c81413c9230d78b4a1e2 Mon Sep 17 00:00:00 2001 From: Rahul Gavande Date: Wed, 31 Dec 2025 14:02:23 +0530 Subject: [PATCH 2/4] Address code review: Move ENAMETOOLONG handling to IPC handler - Move ENAMETOOLONG error handling to generateProposedSitePath IPC handler - Simplify hook to display error message from IPC handler - Update tests to reflect the change Addresses feedback from code review --- src/hooks/tests/use-add-site.test.tsx | 15 ++++++++++++++- src/hooks/use-add-site.ts | 6 +++--- src/ipc-handlers.ts | 3 +++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/hooks/tests/use-add-site.test.tsx b/src/hooks/tests/use-add-site.test.tsx index 9f047267ab..195efe4ad5 100644 --- a/src/hooks/tests/use-add-site.test.tsx +++ b/src/hooks/tests/use-add-site.test.tsx @@ -267,7 +267,7 @@ describe( 'useAddSite', () => { it( 'should set user-friendly error when site name causes ENAMETOOLONG error', async () => { const enametoolongError = new Error( - "Error invoking remote method 'generateProposedSitePath': Error: ENAMETOOLONG: name too long, stat" + 'The site name is too long. Please choose a shorter site name.' ); mockGenerateProposedSitePath.mockRejectedValueOnce( enametoolongError ); @@ -282,6 +282,19 @@ describe( 'useAddSite', () => { ); } ); + it( 'should display error message from generateProposedSitePath', async () => { + const customError = new Error( 'Custom error message' ); + mockGenerateProposedSitePath.mockRejectedValueOnce( customError ); + + const { result } = renderHookWithProvider( () => useAddSite() ); + + await act( async () => { + await result.current.handleSiteNameChange( 'my-site' ); + } ); + + expect( result.current.error ).toBe( 'Custom error message' ); + } ); + it( 'should successfully update site name when path is valid', async () => { mockGenerateProposedSitePath.mockResolvedValueOnce( { path: '/default/path/my-site', diff --git a/src/hooks/use-add-site.ts b/src/hooks/use-add-site.ts index 57c6053587..5206433743 100644 --- a/src/hooks/use-add-site.ts +++ b/src/hooks/use-add-site.ts @@ -237,7 +237,7 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { setSelectedTab, ] ); - const handleSiteNameChange = useCallback( + const handleSiteNameChange = useCallback( async ( name: string ) => { setSiteName( name ); if ( sitePath ) { @@ -271,8 +271,8 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { } setDoesPathContainWordPress( ! isEmpty && isWordPress ); } catch ( err ) { - if ( err instanceof Error && err.message.includes( 'ENAMETOOLONG' ) ) { - setError( __( 'The site name is too long. Please choose a shorter site name.' ) ); + if ( err instanceof Error ) { + setError( err.message ); return; } throw err; diff --git a/src/ipc-handlers.ts b/src/ipc-handlers.ts index a9a4ebdcf3..26961f7d94 100644 --- a/src/ipc-handlers.ts +++ b/src/ipc-handlers.ts @@ -713,6 +713,9 @@ export async function generateProposedSitePath( isWordPress: false, }; } + if ( isErrnoException( err ) && err.code === 'ENAMETOOLONG' ) { + throw new Error( __( 'The site name is too long. Please choose a shorter site name.' ) ); + } throw err; } } From 05a1a68e536e50e3e137665ab13093530be03e8f Mon Sep 17 00:00:00 2001 From: Rahul Gavande Date: Wed, 31 Dec 2025 14:14:06 +0530 Subject: [PATCH 3/4] Revert "Address code review: Move ENAMETOOLONG handling to IPC handler" This reverts commit 6931b1fb807d9e414814c81413c9230d78b4a1e2. --- src/hooks/tests/use-add-site.test.tsx | 15 +-------------- src/hooks/use-add-site.ts | 6 +++--- src/ipc-handlers.ts | 3 --- 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/hooks/tests/use-add-site.test.tsx b/src/hooks/tests/use-add-site.test.tsx index 195efe4ad5..9f047267ab 100644 --- a/src/hooks/tests/use-add-site.test.tsx +++ b/src/hooks/tests/use-add-site.test.tsx @@ -267,7 +267,7 @@ describe( 'useAddSite', () => { it( 'should set user-friendly error when site name causes ENAMETOOLONG error', async () => { const enametoolongError = new Error( - 'The site name is too long. Please choose a shorter site name.' + "Error invoking remote method 'generateProposedSitePath': Error: ENAMETOOLONG: name too long, stat" ); mockGenerateProposedSitePath.mockRejectedValueOnce( enametoolongError ); @@ -282,19 +282,6 @@ describe( 'useAddSite', () => { ); } ); - it( 'should display error message from generateProposedSitePath', async () => { - const customError = new Error( 'Custom error message' ); - mockGenerateProposedSitePath.mockRejectedValueOnce( customError ); - - const { result } = renderHookWithProvider( () => useAddSite() ); - - await act( async () => { - await result.current.handleSiteNameChange( 'my-site' ); - } ); - - expect( result.current.error ).toBe( 'Custom error message' ); - } ); - it( 'should successfully update site name when path is valid', async () => { mockGenerateProposedSitePath.mockResolvedValueOnce( { path: '/default/path/my-site', diff --git a/src/hooks/use-add-site.ts b/src/hooks/use-add-site.ts index 5206433743..57c6053587 100644 --- a/src/hooks/use-add-site.ts +++ b/src/hooks/use-add-site.ts @@ -237,7 +237,7 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { setSelectedTab, ] ); - const handleSiteNameChange = useCallback( + const handleSiteNameChange = useCallback( async ( name: string ) => { setSiteName( name ); if ( sitePath ) { @@ -271,8 +271,8 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { } setDoesPathContainWordPress( ! isEmpty && isWordPress ); } catch ( err ) { - if ( err instanceof Error ) { - setError( err.message ); + if ( err instanceof Error && err.message.includes( 'ENAMETOOLONG' ) ) { + setError( __( 'The site name is too long. Please choose a shorter site name.' ) ); return; } throw err; diff --git a/src/ipc-handlers.ts b/src/ipc-handlers.ts index 26961f7d94..a9a4ebdcf3 100644 --- a/src/ipc-handlers.ts +++ b/src/ipc-handlers.ts @@ -713,9 +713,6 @@ export async function generateProposedSitePath( isWordPress: false, }; } - if ( isErrnoException( err ) && err.code === 'ENAMETOOLONG' ) { - throw new Error( __( 'The site name is too long. Please choose a shorter site name.' ) ); - } throw err; } } From 34e6d431630d283f2fed6b057769b9c58c6650b7 Mon Sep 17 00:00:00 2001 From: Rahul Gavande Date: Wed, 31 Dec 2025 16:20:47 +0530 Subject: [PATCH 4/4] Handle ENAMETOOLONG via isNameTooLong prop --- src/hooks/tests/use-add-site.test.tsx | 11 +++-- src/hooks/use-add-site.ts | 58 +++++++++++++-------------- src/ipc-handlers.ts | 10 +++++ 3 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/hooks/tests/use-add-site.test.tsx b/src/hooks/tests/use-add-site.test.tsx index 9f047267ab..a6bb28a349 100644 --- a/src/hooks/tests/use-add-site.test.tsx +++ b/src/hooks/tests/use-add-site.test.tsx @@ -266,10 +266,13 @@ describe( 'useAddSite', () => { } ); it( 'should set user-friendly error when site name causes ENAMETOOLONG error', async () => { - const enametoolongError = new Error( - "Error invoking remote method 'generateProposedSitePath': Error: ENAMETOOLONG: name too long, stat" - ); - mockGenerateProposedSitePath.mockRejectedValueOnce( enametoolongError ); + mockGenerateProposedSitePath.mockResolvedValueOnce( { + path: '/default/path/very-long-name', + name: 'a'.repeat( 300 ), + isEmpty: false, + isWordPress: false, + isNameTooLong: true, + } ); const { result } = renderHookWithProvider( () => useAddSite() ); diff --git a/src/hooks/use-add-site.ts b/src/hooks/use-add-site.ts index 57c6053587..a71fa23dc7 100644 --- a/src/hooks/use-add-site.ts +++ b/src/hooks/use-add-site.ts @@ -245,38 +245,36 @@ export function useAddSite( options: UseAddSiteOptions = {} ) { } setError( '' ); - try { - const { - path: proposedPath, - isEmpty, - isWordPress, - } = await getIpcApi().generateProposedSitePath( name ); - setProposedSitePath( proposedPath ); + const { + path: proposedPath, + isEmpty, + isWordPress, + isNameTooLong, + } = await getIpcApi().generateProposedSitePath( name ); + setProposedSitePath( proposedPath ); - if ( await siteWithPathAlreadyExists( proposedPath ) ) { - setError( - __( - 'The directory is already associated with another Studio site. Please choose a different site name or a custom local path.' - ) - ); - return; - } - if ( ! isEmpty && ! isWordPress ) { - setError( - __( - 'This directory is not empty. Please select an empty directory or an existing WordPress folder.' - ) - ); - return; - } - setDoesPathContainWordPress( ! isEmpty && isWordPress ); - } catch ( err ) { - if ( err instanceof Error && err.message.includes( 'ENAMETOOLONG' ) ) { - setError( __( 'The site name is too long. Please choose a shorter site name.' ) ); - return; - } - throw err; + if ( isNameTooLong ) { + setError( __( 'The site name is too long. Please choose a shorter site name.' ) ); + return; } + + if ( await siteWithPathAlreadyExists( proposedPath ) ) { + setError( + __( + 'The directory is already associated with another Studio site. Please choose a different site name or a custom local path.' + ) + ); + return; + } + if ( ! isEmpty && ! isWordPress ) { + setError( + __( + 'This directory is not empty. Please select an empty directory or an existing WordPress folder.' + ) + ); + return; + } + setDoesPathContainWordPress( ! isEmpty && isWordPress ); }, [ __, sitePath, siteWithPathAlreadyExists ] ); diff --git a/src/ipc-handlers.ts b/src/ipc-handlers.ts index a9a4ebdcf3..69f7a9f038 100644 --- a/src/ipc-handlers.ts +++ b/src/ipc-handlers.ts @@ -450,6 +450,7 @@ export interface FolderDialogResponse { name: string; isEmpty: boolean; isWordPress: boolean; + isNameTooLong?: boolean; } export async function showSaveAsDialog( event: IpcMainInvokeEvent, options: SaveDialogOptions ) { @@ -713,6 +714,15 @@ export async function generateProposedSitePath( isWordPress: false, }; } + if ( isErrnoException( err ) && err.code === 'ENAMETOOLONG' ) { + return { + path, + name: siteName, + isEmpty: false, + isWordPress: false, + isNameTooLong: true, + }; + } throw err; } }