@@ -18,6 +18,7 @@ const setupClientMocks = ({
1818 downloadPatchFromPpk = mock ( ( ) => Promise . resolve ( ) ) ,
1919 downloadPatchFromPackage = mock ( ( ) => Promise . resolve ( ) ) ,
2020 downloadFullUpdate = mock ( ( ) => Promise . resolve ( ) ) ,
21+ addProgressListener = mock ( ( ) => ( { remove : mock ( ( ) => { } ) } ) ) ,
2122 restartApp = mock ( ( ) => Promise . resolve ( ) ) ,
2223} : {
2324 isFirstTime ?: boolean ;
@@ -27,6 +28,7 @@ const setupClientMocks = ({
2728 downloadPatchFromPpk ?: ReturnType < typeof mock > ;
2829 downloadPatchFromPackage ?: ReturnType < typeof mock > ;
2930 downloadFullUpdate ?: ReturnType < typeof mock > ;
31+ addProgressListener ?: ReturnType < typeof mock > ;
3032 restartApp ?: ReturnType < typeof mock > ;
3133} = { } ) => {
3234 ( globalThis as any ) . __DEV__ = false ;
@@ -69,7 +71,7 @@ const setupClientMocks = ({
6971 isRolledBack : false ,
7072 packageVersion : '1.0.0' ,
7173 pushyNativeEventEmitter : {
72- addListener : mock ( ( ) => ( { remove : mock ( ( ) => { } ) } ) ) ,
74+ addListener : addProgressListener ,
7375 } ,
7476 rolledBackVersion : '' ,
7577 setLocalHashInfo : mock ( ( ) => { } ) ,
@@ -376,15 +378,18 @@ describe('downloadUpdate fallback chain', () => {
376378 downloadPatchFromPpk = mock ( ( ) => Promise . resolve ( ) ) ,
377379 downloadPatchFromPackage = mock ( ( ) => Promise . resolve ( ) ) ,
378380 downloadFullUpdate = mock ( ( ) => Promise . resolve ( ) ) ,
381+ addProgressListener = mock ( ( ) => ( { remove : mock ( ( ) => { } ) } ) ) ,
379382 } : {
380383 downloadPatchFromPpk ?: ReturnType < typeof mock > ;
381384 downloadPatchFromPackage ?: ReturnType < typeof mock > ;
382385 downloadFullUpdate ?: ReturnType < typeof mock > ;
386+ addProgressListener ?: ReturnType < typeof mock > ;
383387 } = { } ) => {
384388 setupClientMocks ( {
385389 downloadPatchFromPpk,
386390 downloadPatchFromPackage,
387391 downloadFullUpdate,
392+ addProgressListener,
388393 } ) ;
389394
390395 // Override setTimeout to skip real backoff delays in retry tests
@@ -396,7 +401,9 @@ describe('downloadUpdate fallback chain', () => {
396401 __esModule : true ,
397402 assertWeb : ( ) => true ,
398403 computeProgress : ( received : number , total : number ) =>
399- total > 0 ? Math . floor ( ( received / total ) * 100 ) : 0 ,
404+ total > 0
405+ ? Math . min ( 100 , Math . max ( 0 , Math . floor ( ( received / total ) * 100 ) ) )
406+ : 0 ,
400407 DEFAULT_FETCH_TIMEOUT_MS : 5000 ,
401408 emptyObj : { } ,
402409 fetchWithTimeout : mock ( ( ) => Promise . resolve ( ) ) ,
@@ -436,6 +443,45 @@ describe('downloadUpdate fallback chain', () => {
436443 expect ( downloadPatchFromPpk ) . toHaveBeenCalledTimes ( 1 ) ;
437444 } ) ;
438445
446+ test ( 'adds computed progress to download progress callbacks' , async ( ) => {
447+ let progressListener :
448+ | ( ( data : {
449+ hash : string ;
450+ received : number ;
451+ total : number ;
452+ } ) => void )
453+ | undefined ;
454+ const addProgressListener = mock (
455+ ( _event : string , listener : typeof progressListener ) => {
456+ progressListener = listener ;
457+ return { remove : mock ( ( ) => { } ) } ;
458+ } ,
459+ ) ;
460+ const onDownloadProgress = mock ( ( ) => { } ) ;
461+ setupDownloadMocks ( {
462+ addProgressListener,
463+ downloadPatchFromPpk : mock ( async ( ) => {
464+ progressListener ?.( {
465+ hash : 'new-hash' ,
466+ received : 1200 ,
467+ total : 1000 ,
468+ } ) ;
469+ } ) ,
470+ } ) ;
471+ const { Pushy, sharedState } = await importFreshClient ( 'dl-progress' ) ;
472+ sharedState . downloadedHash = undefined ;
473+ const client = new Pushy ( { appKey : 'demo-app' } ) ;
474+
475+ await client . downloadUpdate ( updateInfo , onDownloadProgress ) ;
476+
477+ expect ( onDownloadProgress ) . toHaveBeenCalledWith ( {
478+ hash : 'new-hash' ,
479+ received : 1200 ,
480+ total : 1000 ,
481+ progress : 100 ,
482+ } ) ;
483+ } ) ;
484+
439485 test ( 'falls back to pdiff when diff fails' , async ( ) => {
440486 const { downloadPatchFromPpk, downloadPatchFromPackage } =
441487 setupDownloadMocks ( {
@@ -487,6 +533,22 @@ describe('downloadUpdate fallback chain', () => {
487533 ) ;
488534 } ) ;
489535
536+ test ( 'treats negative maxRetries as zero retries' , async ( ) => {
537+ const { downloadFullUpdate } = setupDownloadMocks ( {
538+ downloadPatchFromPpk : mock ( ( ) => Promise . reject ( Error ( 'diff fail' ) ) ) ,
539+ downloadPatchFromPackage : mock ( ( ) => Promise . reject ( Error ( 'pdiff fail' ) ) ) ,
540+ downloadFullUpdate : mock ( ( ) => Promise . reject ( Error ( 'full fail' ) ) ) ,
541+ } ) ;
542+ const { Pushy, sharedState } = await importFreshClient ( 'dl-negative-retries' ) ;
543+ sharedState . downloadedHash = undefined ;
544+ const client = new Pushy ( { appKey : 'demo-app' , maxRetries : - 1 } ) ;
545+
546+ await expect ( client . downloadUpdate ( updateInfo ) ) . rejects . toThrow (
547+ 'error_full_patch_failed' ,
548+ ) ;
549+ expect ( downloadFullUpdate ) . toHaveBeenCalledTimes ( 1 ) ;
550+ } ) ;
551+
490552 test ( 'retries download when maxRetries is set' , async ( ) => {
491553 let callCount = 0 ;
492554 const { downloadFullUpdate } = setupDownloadMocks ( {
0 commit comments