Skip to content
9 changes: 4 additions & 5 deletions src/components/assistant-anchor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,10 @@ export default function Anchor( props: JSX.IntrinsicElements[ 'a' ] & ExtraProps
try {
await getIpcApi().openURL( href );
} catch ( error ) {
getIpcApi().showMessageBox( {
type: 'error',
message: __( 'Failed to open link' ),
detail: __( 'We were unable to open the link. Please try again.' ),
buttons: [ __( 'OK' ) ],
getIpcApi().showErrorMessageBox( {
title: __( 'Failed to open link' ),
message: __( 'We were unable to open the link. Please try again.' ),
error,
} );
Sentry.captureException( error );
}
Expand Down
18 changes: 8 additions & 10 deletions src/components/delete-site.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,19 @@ const DeleteSite = () => {
try {
await deleteSite( selectedSite.id, checkboxChecked );
} catch ( error ) {
await showErrorMessageBox( error, trimmedSiteTitle );
await getIpcApi().showErrorMessageBox( {
title: __( 'Deletion failed' ),
message: sprintf(
__( "We couldn't delete the site '%s'. Please try again" ),
trimmedSiteTitle
),
error,
} );
Sentry.captureException( error );
}
}
};

const showErrorMessageBox = async ( error: unknown, siteTitle: string ) => {
getIpcApi().showMessageBox( {
type: 'warning',
message: __( 'Deletion failed' ),
detail: sprintf( __( "We couldn't delete the site '%s'. Please try again" ), siteTitle ),
buttons: [ __( 'OK' ) ],
} );
};

const getTrimmedSiteTitle = ( name: string ) =>
name.length > MAX_LENGTH_SITE_TITLE
? `${ name.substring( 0, MAX_LENGTH_SITE_TITLE - 3 ) }...`
Expand Down
16 changes: 8 additions & 8 deletions src/components/tests/assistant-anchor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe( 'Anchor', () => {
( useSiteDetails as jest.Mock ).mockReturnValue( {} );
( getIpcApi as jest.Mock ).mockReturnValue( {
openURL: jest.fn( () => Promise.resolve() ),
showMessageBox: jest.fn(),
showErrorMessageBox: jest.fn(),
} );
} );

Expand Down Expand Up @@ -79,9 +79,10 @@ describe( 'Anchor', () => {
} );

it( 'should gracefully handle link open failures', async () => {
const error = new Error( 'Failed to open link' );
( getIpcApi as jest.Mock ).mockReturnValue( {
openURL: jest.fn( () => Promise.reject( new Error( 'Failed to open link' ) ) ),
showMessageBox: jest.fn(),
openURL: jest.fn( () => Promise.reject( error ) ),
showErrorMessageBox: jest.fn(),
} );
render( <Anchor href="https://example.com" children="Example link" /> );

Expand All @@ -90,11 +91,10 @@ describe( 'Anchor', () => {
// Await asynchronous openURL execution
await new Promise( process.nextTick );

expect( getIpcApi().showMessageBox ).toHaveBeenCalledWith( {
type: 'error',
message: 'Failed to open link',
detail: 'We were unable to open the link. Please try again.',
buttons: [ 'OK' ],
expect( getIpcApi().showErrorMessageBox ).toHaveBeenCalledWith( {
title: 'Failed to open link',
message: 'We were unable to open the link. Please try again.',
error,
} );
expect( Sentry.captureException ).toHaveBeenCalled();
} );
Expand Down
15 changes: 8 additions & 7 deletions src/components/tests/header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function mockGetIpcApi( mocks: Record< string, jest.Mock > ) {
getSnapshots: jest.fn( () => Promise.resolve( [] ) ),
saveSnapshotsToStorage: jest.fn( () => Promise.resolve() ),
startServer: jest.fn( () => Promise.resolve( { running: true } ) ),
showMessageBox: jest.fn(),
showErrorMessageBox: jest.fn(),
...mocks,
} );
}
Expand Down Expand Up @@ -54,9 +54,10 @@ describe( 'Header', () => {
describe( 'when starting a server fails', () => {
it( 'should display an error message', async () => {
const user = userEvent.setup();
const error = new Error( 'Failed to start the server' );
mockGetIpcApi( {
startServer: jest.fn( () => {
throw new Error( 'Failed to start the server' );
throw error;
} ),
stopServer: jest.fn( () => Promise.resolve( { running: false } ) ),
} );
Expand All @@ -72,12 +73,12 @@ describe( 'Header', () => {

expect( mockedGetIpcApi().startServer ).toHaveBeenCalledTimes( 1 );
expect( screen.getByText( 'Start' ) ).toBeVisible();
expect( mockedGetIpcApi().showMessageBox ).toHaveBeenCalledTimes( 1 );
expect( mockedGetIpcApi().showMessageBox ).toHaveBeenCalledWith( {
type: 'error',
message: 'Failed to start the site server',
detail:
expect( mockedGetIpcApi().showErrorMessageBox ).toHaveBeenCalledTimes( 1 );
expect( mockedGetIpcApi().showErrorMessageBox ).toHaveBeenCalledWith( {
title: 'Failed to start the site server',
message:
"Please verify your site's local path directory contains the standard WordPress installation files and try again. If this problem persists, please contact support.",
error,
} );
} );
} );
Expand Down
20 changes: 13 additions & 7 deletions src/hooks/tests/use-import-export.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ beforeEach( () => {
jest.clearAllMocks();
( getIpcApi as jest.Mock ).mockReturnValue( {
showSaveAsDialog: jest.fn(),
showMessageBox: jest.fn().mockResolvedValue( { response: 0, checkboxChecked: false } ),
showErrorMessageBox: jest.fn(),
showNotification: jest.fn(),
exportSite: jest.fn().mockReturnValue( true ),
importSite: jest.fn().mockReturnValue( selectedSite ),
Expand Down Expand Up @@ -84,9 +84,12 @@ describe( 'useImportExport hook', () => {
},
SITE_ID
);
expect( getIpcApi().showMessageBox ).toHaveBeenCalledWith(
expect.objectContaining( { type: 'error', message: 'Failed exporting site' } )
);
expect( getIpcApi().showErrorMessageBox ).toHaveBeenCalledWith( {
title: 'Failed exporting site',
message:
'An error occurred while exporting the site. If this problem persists, please contact support.',
error: 'error',
} );
} );

it( 'exports database', async () => {
Expand Down Expand Up @@ -233,9 +236,12 @@ describe( 'useImportExport hook', () => {
path: 'backup.zip',
},
} );
expect( getIpcApi().showMessageBox ).toHaveBeenCalledWith(
expect.objectContaining( { type: 'error', message: 'Failed importing site' } )
);
expect( getIpcApi().showErrorMessageBox ).toHaveBeenCalledWith( {
title: 'Failed importing site',
message:
'An error occurred while importing the site. Verify the file is a valid Jetpack backup, Local, Playground or .sql database file and try again. If this problem persists, please contact support.',
error: 'error',
} );
} );

it( 'does not import if another import is running', async () => {
Expand Down
15 changes: 8 additions & 7 deletions src/hooks/tests/use-update-demo-site.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jest.mock( '../../hooks/use-auth' );
jest.mock( '../../lib/get-ipc-api', () => ( {
getIpcApi: jest.fn().mockReturnValue( {
archiveSite: jest.fn().mockResolvedValue( { zipContent: new Blob( [ 'zipContent' ] ) } ),
showMessageBox: jest.fn().mockResolvedValue( { response: 1 } ), // Assuming '1' is the cancel button
showErrorMessageBox: jest.fn().mockResolvedValue( { response: 1 } ), // Assuming '1' is the cancel button
getWpVersion: jest.fn().mockResolvedValue( '6.5' ),
} ),
} ) );
Expand Down Expand Up @@ -108,7 +108,8 @@ describe( 'useUpdateDemoSite', () => {
} );

it( 'when an update fails, ensure an alert is triggered', async () => {
clientReqPost.mockRejectedValue( new Error( 'Update failed' ) );
const error = new Error( 'Update failed' );
clientReqPost.mockRejectedValue( error );

const { result } = renderHook( () => useUpdateDemoSite(), { wrapper } );

Expand All @@ -118,11 +119,11 @@ describe( 'useUpdateDemoSite', () => {
} );

// Assert that an alert is displayed to inform users of the failure
expect( getIpcApi().showMessageBox ).toHaveBeenCalledWith(
expect.objectContaining( {
type: 'warning',
} )
);
expect( getIpcApi().showErrorMessageBox ).toHaveBeenCalledWith( {
title: 'Update failed',
message: "We couldn't update the Test Site demo site. Please try again.",
error,
} );

// Assert that 'isDemoSiteUpdating' is set back to false
expect( result.current.isDemoSiteUpdating( mockLocalSite.id ) ).toBe( false );
Expand Down
115 changes: 63 additions & 52 deletions src/hooks/use-archive-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,67 +77,78 @@ export function useArchiveSite() {
setUploadingSites( ( _uploadingSites ) => ( { ..._uploadingSites, [ siteId ]: false } ) );
return;
}
const { zipContent, zipPath, exceedsSizeLimit } = await getIpcApi().archiveSite( siteId );
if ( exceedsSizeLimit ) {
alert(
sprintf(
__(
'The site exceeds the maximum size of %dMB. Please remove some files and try again.'
),
SIZE_LIMIT_MB
)
);
setUploadingSites( ( _uploadingSites ) => ( { ..._uploadingSites, [ siteId ]: false } ) );
getIpcApi().removeTemporalFile( zipPath );
return;
}

const file = new File( [ zipContent ], 'loca-env-site-1.zip', {
type: 'application/zip',
} );

const formData = [ [ 'import', file ] ];
const wordpressVersion = await getIpcApi().getWpVersion( siteId );
if ( wordpressVersion.length >= 3 ) {
formData.push( [ 'wordpress_version', wordpressVersion ] );
}

try {
const response: {
atomic_site_id: number;
domain_name: string;
admin_pass: string;
admin_user: string;
job_id: number;
} = await client.req.post( {
path: '/jurassic-ninja/create-new-site-from-zip',
apiNamespace: 'wpcom/v2',
formData,
} );
addSnapshot( {
url: response.domain_name,
atomicSiteId: response.atomic_site_id,
localSiteId: siteId,
date: new Date().getTime(),
isLoading: true,
const { zipContent, zipPath, exceedsSizeLimit } = await getIpcApi().archiveSite( siteId );
if ( exceedsSizeLimit ) {
alert(
sprintf(
__(
'The site exceeds the maximum size of %dMB. Please remove some files and try again.'
),
SIZE_LIMIT_MB
)
);
setUploadingSites( ( _uploadingSites ) => ( { ..._uploadingSites, [ siteId ]: false } ) );
getIpcApi().removeTemporalFile( zipPath );
return;
}

const file = new File( [ zipContent ], 'loca-env-site-1.zip', {
type: 'application/zip',
} );
} catch ( error ) {
if ( isWpcomNetworkError( error ) ) {
if ( error.code in errorMessages ) {
alert( errorMessages[ error.code as keyof typeof errorMessages ] );

const formData = [ [ 'import', file ] ];
const wordpressVersion = await getIpcApi().getWpVersion( siteId );
if ( wordpressVersion.length >= 3 ) {
formData.push( [ 'wordpress_version', wordpressVersion ] );
}

try {
const response: {
atomic_site_id: number;
domain_name: string;
admin_pass: string;
admin_user: string;
job_id: number;
} = await client.req.post( {
path: '/jurassic-ninja/create-new-site-from-zip',
apiNamespace: 'wpcom/v2',
formData,
} );
addSnapshot( {
url: response.domain_name,
atomicSiteId: response.atomic_site_id,
localSiteId: siteId,
date: new Date().getTime(),
isLoading: true,
} );
} catch ( error ) {
if ( isWpcomNetworkError( error ) ) {
if ( error.code in errorMessages ) {
alert( errorMessages[ error.code as keyof typeof errorMessages ] );
} else {
alert( __( 'Error sharing site. Please try again.' ) );
}
if ( error.code !== 'rest_site_limit_reached' ) {
Sentry.captureException( error );
}
} else {
alert( __( 'Error sharing site. Please try again.' ) );
}
if ( error.code !== 'rest_site_limit_reached' ) {
alert( __( 'Error sharing site. Please contact support.' ) );
Sentry.captureException( error );
}
} else {
alert( __( 'Error sharing site. Please contact support.' ) );
Sentry.captureException( error );
} finally {
getIpcApi().removeTemporalFile( zipPath );
}
} catch ( error ) {
getIpcApi().showErrorMessageBox( {
title: __( 'Archiving failed' ),
message: __( 'Error sharing site. Please try again.' ),
error,
} );
Sentry.captureException( error );
} finally {
setUploadingSites( ( _uploadingSites ) => ( { ..._uploadingSites, [ siteId ]: false } ) );
getIpcApi().removeTemporalFile( zipPath );
}
},
[ __, addSnapshot, client, errorMessages, fetchSnapshotUsage, setUploadingSites ]
Expand Down
26 changes: 12 additions & 14 deletions src/hooks/use-import-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,13 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode
} ) );

const wasSiteRunning = selectedSite.running;
const handleImportError = async () => {
await getIpcApi().showMessageBox( {
type: 'error',
message: __( 'Failed importing site' ),
detail: __(
const handleImportError = async ( error?: unknown ) => {
await getIpcApi().showErrorMessageBox( {
title: __( 'Failed importing site' ),
message: __(
'An error occurred while importing the site. Verify the file is a valid Jetpack backup, Local, Playground or .sql database file and try again. If this problem persists, please contact support.'
),
buttons: [ __( 'OK' ) ],
error,
} );
setImportState( ( { [ selectedSite.id ]: currentProgress, ...rest } ) => ( {
...rest,
Expand Down Expand Up @@ -130,7 +129,7 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode
} );
}
} catch ( error ) {
await handleImportError();
await handleImportError( error );
} finally {
if ( wasSiteRunning ) {
startServer( selectedSite.id );
Expand Down Expand Up @@ -251,14 +250,13 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode
[ selectedSite.id ]: INITIAL_EXPORT_STATE,
} ) );

const handleExportError = async () =>
getIpcApi().showMessageBox( {
type: 'error',
message: __( 'Failed exporting site' ),
detail: __(
const handleExportError = async ( error?: unknown ) =>
getIpcApi().showErrorMessageBox( {
title: __( 'Failed exporting site' ),
message: __(
'An error occurred while exporting the site. If this problem persists, please contact support.'
),
buttons: [ __( 'OK' ) ],
error,
} );

try {
Expand All @@ -278,7 +276,7 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode
return options.backupFile;
} catch ( error ) {
Sentry.captureException( error );
await handleExportError();
await handleExportError( error );
} finally {
setExportState( ( { [ selectedSite.id ]: currentProgress, ...rest } ) => ( {
...rest,
Expand Down
Loading