@@ -30,6 +30,7 @@ vi.mock('electron-updater', () => ({
3030 checkForUpdates : vi . fn ( ) ,
3131 downloadUpdate : vi . fn ( ( ) => Promise . resolve ( [ ] ) ) ,
3232 quitAndInstall : vi . fn ( ) ,
33+ disableDifferentialDownload : false ,
3334 } ,
3435 } ,
3536} ) )
@@ -69,6 +70,7 @@ afterEach(() => {
6970 autoUpdater . autoInstallOnAppQuit = undefined as unknown as boolean
7071 autoUpdater . allowPrerelease = undefined as unknown as boolean
7172 autoUpdater . allowDowngrade = undefined as unknown as boolean
73+ autoUpdater . disableDifferentialDownload = false
7274 ; ( autoUpdater as unknown as { _channel : string | null } ) . _channel = null
7375} )
7476
@@ -241,6 +243,7 @@ describe('strict update consent', () => {
241243 expect ( localAutoUpdater . allowDowngrade ) . toBe ( false )
242244 expect ( localAutoUpdater . autoDownload ) . toBe ( false )
243245 expect ( localAutoUpdater . autoInstallOnAppQuit ) . toBe ( false )
246+ expect ( localAutoUpdater . disableDifferentialDownload ) . toBe ( true )
244247 expect ( localAutoUpdater . checkForUpdates ) . not . toHaveBeenCalled ( )
245248 expect ( localAutoUpdater . downloadUpdate ) . not . toHaveBeenCalled ( )
246249 expect ( localAutoUpdater . quitAndInstall ) . not . toHaveBeenCalled ( )
@@ -339,6 +342,7 @@ describe('strict update consent', () => {
339342 const {
340343 getUpdateState : getLocalUpdateState ,
341344 initUpdater : initLocalUpdater ,
345+ startUpdateDownload : startLocalUpdateDownload ,
342346 } = await import ( '../src/updater' )
343347 vi . mocked ( localApp . getPath ) . mockReturnValue ( directory )
344348 Object . defineProperty ( localApp , 'isPackaged' , { configurable : true , value : true } )
@@ -366,6 +370,7 @@ describe('strict update consent', () => {
366370 releaseDate : '2026-08-22T12:00:00.000Z' ,
367371 releaseNotes : [ { note : 'First change' } , { note : null } , { note : 'Second change' } ] ,
368372 } )
373+ startLocalUpdateDownload ( )
369374 progress ?.( { percent : 42.5 , transferred : 425 , total : 1_000 , bytesPerSecond : 85 } )
370375
371376 expect ( getLocalUpdateState ( ) ) . toMatchObject ( {
@@ -381,6 +386,38 @@ describe('strict update consent', () => {
381386 } )
382387 } )
383388
389+ it ( 'keeps an available update downloadable when automatic checks are enabled' , async ( ) => {
390+ vi . resetModules ( )
391+ const directory = temporaryDirectory ( )
392+ writeFileSync ( join ( directory , 'app-update.yml' ) , '' , 'utf8' )
393+ writeFileSync ( join ( directory , 'update-settings.json' ) , '{"autoUpdate":false}\n' , 'utf8' )
394+ const { app : localApp } = await import ( 'electron' )
395+ const { default : localElectronUpdater } = await import ( 'electron-updater' )
396+ const {
397+ getUpdateState : getLocalUpdateState ,
398+ initUpdater : initLocalUpdater ,
399+ setAutoUpdate : setLocalAutoUpdate ,
400+ startUpdateDownload : startLocalUpdateDownload ,
401+ } = await import ( '../src/updater' )
402+ const localAutoUpdater = localElectronUpdater . autoUpdater
403+ vi . mocked ( localApp . getPath ) . mockReturnValue ( directory )
404+ Object . defineProperty ( localApp , 'isPackaged' , { configurable : true , value : true } )
405+ Object . defineProperty ( process , 'resourcesPath' , { configurable : true , value : directory } )
406+
407+ initLocalUpdater ( ( ) => undefined )
408+ const available = vi . mocked ( localAutoUpdater . on ) . mock . calls . find (
409+ ( [ event ] ) => event === 'update-available' ,
410+ ) ?. [ 1 ] as ( ( info : { version : string } ) => void ) | undefined
411+ available ?.( { version : '1.2.3' } )
412+
413+ expect ( setLocalAutoUpdate ( true ) ) . toMatchObject ( { status : 'available' , availableVersion : '1.2.3' } )
414+ expect ( localAutoUpdater . checkForUpdates ) . not . toHaveBeenCalled ( )
415+ expect ( startLocalUpdateDownload ( ) ) . toMatchObject ( { status : 'downloading' } )
416+ expect ( localAutoUpdater . downloadUpdate ) . toHaveBeenCalledOnce ( )
417+ setLocalAutoUpdate ( false )
418+ expect ( getLocalUpdateState ( ) ) . toMatchObject ( { status : 'downloading' } )
419+ } )
420+
384421 it ( 'downloads and installs only through separate explicit actions' , async ( ) => {
385422 vi . resetModules ( )
386423 const directory = temporaryDirectory ( )
@@ -420,6 +457,55 @@ describe('strict update consent', () => {
420457 expect ( readUpdateSettings ( directory ) ) . toMatchObject ( { pendingInstallVersion : '1.2.3' } )
421458 } )
422459
460+ it ( 'does not return to downloading after the update is ready' , async ( ) => {
461+ vi . resetModules ( )
462+ const directory = temporaryDirectory ( )
463+ writeFileSync ( join ( directory , 'app-update.yml' ) , '' , 'utf8' )
464+ writeFileSync ( join ( directory , 'update-settings.json' ) , '{"autoUpdate":false}\n' , 'utf8' )
465+ const { app : localApp } = await import ( 'electron' )
466+ const { default : localElectronUpdater } = await import ( 'electron-updater' )
467+ const {
468+ getUpdateState : getLocalUpdateState ,
469+ initUpdater : initLocalUpdater ,
470+ startUpdateDownload : startLocalUpdateDownload ,
471+ } = await import ( '../src/updater' )
472+ const localAutoUpdater = localElectronUpdater . autoUpdater
473+ vi . mocked ( localApp . getPath ) . mockReturnValue ( directory )
474+ Object . defineProperty ( localApp , 'isPackaged' , { configurable : true , value : true } )
475+ Object . defineProperty ( process , 'resourcesPath' , { configurable : true , value : directory } )
476+
477+ initLocalUpdater ( ( ) => undefined )
478+ const available = vi . mocked ( localAutoUpdater . on ) . mock . calls . find (
479+ ( [ event ] ) => event === 'update-available' ,
480+ ) ?. [ 1 ] as ( ( info : { version : string } ) => void ) | undefined
481+ const progress = vi . mocked ( localAutoUpdater . on ) . mock . calls . find (
482+ ( [ event ] ) => event === 'download-progress' ,
483+ ) ?. [ 1 ] as ( ( info : {
484+ percent : number
485+ transferred : number
486+ total : number
487+ bytesPerSecond : number
488+ } ) => void ) | undefined
489+ const downloaded = vi . mocked ( localAutoUpdater . on ) . mock . calls . find (
490+ ( [ event ] ) => event === 'update-downloaded' ,
491+ ) ?. [ 1 ] as ( ( info : { version : string } ) => void ) | undefined
492+
493+ available ?.( { version : '1.2.3' } )
494+ startLocalUpdateDownload ( )
495+ progress ?.( { percent : 100 , transferred : 1_000 , total : 1_000 , bytesPerSecond : 80 } )
496+ downloaded ?.( { version : '1.2.3' } )
497+ progress ?.( { percent : 1 , transferred : 10 , total : 1_000 , bytesPerSecond : 20 } )
498+
499+ expect ( getLocalUpdateState ( ) ) . toMatchObject ( {
500+ status : 'downloaded' ,
501+ availableVersion : '1.2.3' ,
502+ percent : 100 ,
503+ transferred : 1_000 ,
504+ total : 1_000 ,
505+ } )
506+ expect ( localAutoUpdater . downloadUpdate ) . toHaveBeenCalledOnce ( )
507+ } )
508+
423509 it ( 'does not let a scheduled check overwrite a downloaded update' , async ( ) => {
424510 vi . useFakeTimers ( )
425511 try {
0 commit comments