diff --git a/apps/studio/__mocks__/electron.ts b/apps/studio/__mocks__/electron.ts index e37907fe82..7def34fb29 100644 --- a/apps/studio/__mocks__/electron.ts +++ b/apps/studio/__mocks__/electron.ts @@ -86,6 +86,10 @@ export const autoUpdater = { on: vi.fn(), }; +export const clipboard = { + writeText: vi.fn(), +}; + export const session = { defaultSession: { setPermissionRequestHandler: vi.fn(), diff --git a/apps/studio/src/tests/updates.test.ts b/apps/studio/src/tests/updates.test.ts index 03afe86fb4..f50603f871 100644 --- a/apps/studio/src/tests/updates.test.ts +++ b/apps/studio/src/tests/updates.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { app, dialog, shell, type MessageBoxOptions } from 'electron'; +import { app, clipboard, dialog, shell, type MessageBoxOptions } from 'electron'; import * as Sentry from '@sentry/electron/main'; import { vi } from 'vitest'; import { manualCheckForUpdates } from 'src/updates'; @@ -68,6 +68,55 @@ describe( 'Linux updater', () => { ); } ); + it( 'copies the install command to the clipboard when the user clicks the primary button', async () => { + global.fetch = vi.fn().mockResolvedValue( { + status: 200, + ok: true, + json: async () => ( { + version: '1.9.0', + downloadUrl: 'https://appscdn.example.com/path/studio_1.9.0_arm64.deb', + } ), + } as Response ); + vi.mocked( dialog.showMessageBox ).mockResolvedValue( { + response: 0, + checkboxChecked: false, + } ); + + await manualCheckForUpdates(); + + await vi.waitFor( () => { + expect( shell.openExternal ).toHaveBeenCalled(); + } ); + + expect( clipboard.writeText ).toHaveBeenCalledWith( + 'sudo apt install ~/Downloads/studio_1.9.0_arm64.deb' + ); + } ); + + it( 'does not copy to the clipboard or open the browser when the user dismisses the dialog', async () => { + global.fetch = vi.fn().mockResolvedValue( { + status: 200, + ok: true, + json: async () => ( { + version: '1.9.0', + downloadUrl: 'https://appscdn.example.com/path/studio_1.9.0_arm64.deb', + } ), + } as Response ); + vi.mocked( dialog.showMessageBox ).mockResolvedValue( { + response: 1, + checkboxChecked: false, + } ); + + await manualCheckForUpdates(); + + await vi.waitFor( () => { + expect( dialog.showMessageBox ).toHaveBeenCalled(); + } ); + + expect( clipboard.writeText ).not.toHaveBeenCalled(); + expect( shell.openExternal ).not.toHaveBeenCalled(); + } ); + it( 'shows "No updates available" on a manual check when the server returns 204', async () => { global.fetch = vi.fn().mockResolvedValue( { status: 204, diff --git a/apps/studio/src/updates.ts b/apps/studio/src/updates.ts index 38b55ce0b1..9455ad2ef2 100644 --- a/apps/studio/src/updates.ts +++ b/apps/studio/src/updates.ts @@ -1,4 +1,4 @@ -import { app, autoUpdater, dialog } from 'electron'; +import { app, autoUpdater, clipboard, dialog } from 'electron'; import * as Sentry from '@sentry/electron/main'; import { sprintf, __ } from '@wordpress/i18n'; import { AUTO_UPDATE_INTERVAL_MS } from 'src/constants'; @@ -300,20 +300,20 @@ async function showLinuxUpdateAvailableNotice( version: string, downloadUrl: str const mainWindow = await getMainWindow(); const command = `sudo apt install ~/Downloads/${ debFilenameFromUrl( downloadUrl ) }`; - const introLine = __( - 'After downloading, quit Studio and run this command from a terminal to install:' + const actionDescription = __( + 'Clicking the button below will download the new package and copy the install command to your clipboard.' ); - const doubleClickHint = __( - 'On some distributions, double-clicking the downloaded file may also work.' + const followUpInstruction = __( + 'Once the download finishes, quit Studio, open a terminal, and paste the command to install:' ); const { response } = await dialog.showMessageBox( mainWindow, { type: 'info', - buttons: [ __( 'Download' ), __( 'Later' ) ], + buttons: [ __( 'Download & copy command' ), __( 'Later' ) ], title: __( 'New Version Available' ), // translators: %s is the version number, e.g. "1.9.0". message: sprintf( __( 'Studio %s is available' ), version ), - detail: `${ introLine }\n\n${ command }\n\n${ doubleClickHint }`, + detail: `${ actionDescription }\n\n${ followUpInstruction }\n\n${ command }`, defaultId: 0, cancelId: 1, } ); @@ -330,6 +330,7 @@ async function showLinuxUpdateAvailableNotice( version: string, downloadUrl: str ); return; } + clipboard.writeText( command ); void shellOpenExternalWrapper( parsedUrl.toString() ); } catch { Sentry.captureException( new Error( `Malformed downloadUrl: ${ downloadUrl }` ) );