@@ -21,6 +21,9 @@ const buildHeadResponse = (contentLength: string | null, ok = true): Response =>
2121 headers : { get : ( name : string ) => ( name . toLowerCase ( ) === 'content-length' ? contentLength : null ) }
2222 } ) as unknown as Response
2323
24+ // We expose this helper to prove the new implementation *ignores* the API
25+ // even when Chrome reports a value. Pre-fix, this dial drove the estimate;
26+ // post-fix, it must have zero effect on the result.
2427const setNavigatorConnection = ( downlink : number | undefined ) : void => {
2528 Object . defineProperty ( window . navigator , 'connection' , {
2629 configurable : true ,
@@ -45,45 +48,63 @@ describe('estimateDownloadHoldMs', () => {
4548 clearNavigatorConnection ( )
4649 } )
4750
48- describe ( 'when the HEAD request returns a Content-Length and the browser exposes downlink ' , ( ) => {
51+ describe ( 'when the HEAD request returns a Content-Length' , ( ) => {
4952 // ~4.4 MB DMG — the actual size of the current Decentraland macOS
5053 // installer, verified via curl HEAD on the gateway. Tauri reuses the
5154 // system WebView so the bundle stays small.
5255 const DMG_SIZE_BYTES = 4_618_782
5356
54- beforeEach ( ( ) => {
57+ it ( 'should land between MIN and MAX for the real DMG size at the assumed broadband rate' , async ( ) => {
58+ // 4.4 MB at 100 Mbps ≈ 350ms transfer + 500ms safety margin → ~850ms,
59+ // just above the 800ms minimum.
5560 mockFetch . mockResolvedValue ( buildHeadResponse ( String ( DMG_SIZE_BYTES ) ) )
56- } )
5761
58- it ( 'should compute a sub-second hold for fiber-class connections on the real DMG size' , async ( ) => {
59- // 4.4 MB at 100 Mbps ≈ 350ms transfer + 500ms safety margin → ~850ms,
60- // which clears the 800ms minimum so the math result is what we expect.
61- setNavigatorConnection ( 100 )
6262 const ms = await estimateDownloadHoldMs ( 'https://example.com/file.dmg' )
63- expect ( ms ) . toBeGreaterThanOrEqual ( 800 )
63+
64+ expect ( ms ) . toBeGreaterThanOrEqual ( ESTIMATE_MIN_HOLD_MS )
6465 expect ( ms ) . toBeLessThanOrEqual ( 1100 )
6566 } )
6667
67- it ( 'should produce a longer hold on a slow connection but never beyond MAX' , async ( ) => {
68- // 4.4 MB at 1 Mbps ≈ 35s pure transfer, way over ESTIMATE_MAX_HOLD_MS.
69- setNavigatorConnection ( 1 )
68+ it ( 'should clamp to MAX for files large enough that the pure transfer exceeds the cap' , async ( ) => {
69+ // 200 MB at 100 Mbps ≈ 16s of pure transfer — well past MAX.
70+ mockFetch . mockResolvedValue ( buildHeadResponse ( String ( 200 * 1024 * 1024 ) ) )
71+
7072 const ms = await estimateDownloadHoldMs ( 'https://example.com/file.dmg' )
73+
7174 expect ( ms ) . toBe ( ESTIMATE_MAX_HOLD_MS )
7275 } )
7376
74- it ( 'should produce a mid-range hold on typical broadband ' , async ( ) => {
75- // 4.4 MB at 25 Mbps ≈ 1.4s + 500ms margin → ~1.9s. Well within MIN/MAX.
76- setNavigatorConnection ( 25 )
77+ it ( 'should clamp to MIN for tiny files so the modal does not blink and vanish ' , async ( ) => {
78+ mockFetch . mockResolvedValue ( buildHeadResponse ( '512' ) ) // 512 bytes
79+
7780 const ms = await estimateDownloadHoldMs ( 'https://example.com/file.dmg' )
78- expect ( ms ) . toBeGreaterThanOrEqual ( 1500 )
79- expect ( ms ) . toBeLessThanOrEqual ( 2500 )
81+
82+ expect ( ms ) . toBe ( ESTIMATE_MIN_HOLD_MS )
83+ } )
84+
85+ it ( 'should ignore navigator.connection.downlink (Chrome caps it at ~10 Mbps, breaking the estimate)' , async ( ) => {
86+ // Pre-fix this would have driven the estimate up to MAX. Post-fix it
87+ // must have zero effect: the result for a fixed file size is identical
88+ // regardless of what the (mis)reported downlink claims.
89+ mockFetch . mockResolvedValue ( buildHeadResponse ( String ( DMG_SIZE_BYTES ) ) )
90+
91+ setNavigatorConnection ( 1 )
92+ const slow = await estimateDownloadHoldMs ( 'https://example.com/file.dmg' )
93+
94+ setNavigatorConnection ( 100 )
95+ const fast = await estimateDownloadHoldMs ( 'https://example.com/file.dmg' )
96+
97+ clearNavigatorConnection ( )
98+ const missing = await estimateDownloadHoldMs ( 'https://example.com/file.dmg' )
99+
100+ expect ( slow ) . toBe ( fast )
101+ expect ( fast ) . toBe ( missing )
80102 } )
81103 } )
82104
83105 describe ( 'when the HEAD request fails' , ( ) => {
84106 it ( 'should return the minimum hold instead of guessing' , async ( ) => {
85107 mockFetch . mockRejectedValue ( new TypeError ( 'CORS blocked' ) )
86- setNavigatorConnection ( 50 )
87108
88109 const ms = await estimateDownloadHoldMs ( 'https://example.com/file.dmg' )
89110
@@ -94,7 +115,6 @@ describe('estimateDownloadHoldMs', () => {
94115 describe ( 'when the HEAD response is non-2xx (403, 405, etc.)' , ( ) => {
95116 it ( 'should treat it like a missing Content-Length and return the minimum hold' , async ( ) => {
96117 mockFetch . mockResolvedValue ( buildHeadResponse ( String ( 80 * 1024 * 1024 ) , false ) )
97- setNavigatorConnection ( 50 )
98118
99119 const ms = await estimateDownloadHoldMs ( 'https://example.com/file.dmg' )
100120
@@ -105,33 +125,6 @@ describe('estimateDownloadHoldMs', () => {
105125 describe ( 'when the HEAD response omits Content-Length' , ( ) => {
106126 it ( 'should return the minimum hold' , async ( ) => {
107127 mockFetch . mockResolvedValue ( buildHeadResponse ( null ) )
108- setNavigatorConnection ( 50 )
109-
110- const ms = await estimateDownloadHoldMs ( 'https://example.com/file.dmg' )
111-
112- expect ( ms ) . toBe ( ESTIMATE_MIN_HOLD_MS )
113- } )
114- } )
115-
116- describe ( 'when the browser does not expose navigator.connection (Safari, Firefox)' , ( ) => {
117- it ( 'should fall back to the default downlink and still produce an adaptive hold' , async ( ) => {
118- // 4.4 MB DMG at the default 50 Mbps ≈ 700ms + 500ms margin ≈ 1.2s.
119- // Lands between MIN and MAX so the assertion exercises the adaptive
120- // arithmetic rather than the clamps.
121- mockFetch . mockResolvedValue ( buildHeadResponse ( String ( 4_618_782 ) ) )
122- clearNavigatorConnection ( )
123-
124- const ms = await estimateDownloadHoldMs ( 'https://example.com/file.dmg' )
125-
126- expect ( ms ) . toBeGreaterThan ( ESTIMATE_MIN_HOLD_MS )
127- expect ( ms ) . toBeLessThan ( ESTIMATE_MAX_HOLD_MS )
128- } )
129- } )
130-
131- describe ( 'when the file is tiny' , ( ) => {
132- it ( 'should clamp to the minimum hold so the modal does not vanish instantly' , async ( ) => {
133- mockFetch . mockResolvedValue ( buildHeadResponse ( '512' ) ) // 512 bytes
134- setNavigatorConnection ( 100 )
135128
136129 const ms = await estimateDownloadHoldMs ( 'https://example.com/file.dmg' )
137130
@@ -216,9 +209,8 @@ describe('streamOrFallback', () => {
216209
217210 it ( 'should resolve promptly when the signal aborts mid-hold' , async ( ) => {
218211 // Long estimated hold so the test exercises a real wait window:
219- // 80 MB at 5 Mbps caps at MAX_HOLD_MS.
220- mockFetch . mockResolvedValue ( buildHeadResponse ( String ( 80 * 1024 * 1024 ) ) )
221- setNavigatorConnection ( 5 )
212+ // 200 MB at the assumed 100 Mbps caps at MAX_HOLD_MS.
213+ mockFetch . mockResolvedValue ( buildHeadResponse ( String ( 200 * 1024 * 1024 ) ) )
222214
223215 let resolved = false
224216 streamOrFallback ( {
0 commit comments