@@ -353,6 +353,58 @@ describe('offlineNavigations build artifacts', () => {
353353 } )
354354 }
355355
356+ async function prefetchDynamicPatternReplayData (
357+ browser : Awaited < ReturnType < typeof next . browser > >
358+ ) {
359+ await browser . elementById ( 'prefetch-dynamic-pattern-source' ) . click ( )
360+ await retry ( async ( ) => {
361+ const routeRecords =
362+ await readPersistedOfflineNavigationRouteRecords ( browser )
363+ expect (
364+ routeRecords . some ( ( record ) =>
365+ record . route . pathname . includes ( '/dynamic-prefetch/learned' )
366+ )
367+ ) . toBe ( true )
368+ } )
369+
370+ await browser . elementById ( 'prefetch-dynamic-pattern-target' ) . click ( )
371+ await retry ( async ( ) => {
372+ const routeRecords =
373+ await readPersistedOfflineNavigationRouteRecords ( browser )
374+ expect (
375+ routeRecords . some ( ( record ) =>
376+ record . route . pathname . includes ( '/dynamic-prefetch/learned' )
377+ )
378+ ) . toBe ( true )
379+ expect (
380+ routeRecords . some ( ( record ) =>
381+ record . route . pathname . includes ( '/dynamic-prefetch/replayed' )
382+ )
383+ ) . toBe ( false )
384+
385+ const segmentRecords =
386+ await readPersistedOfflineNavigationSegmentRecords ( browser )
387+ const replayedSegmentRecords = segmentRecords . filter ( ( record ) =>
388+ record . key . includes ( 'replayed' )
389+ )
390+ expect (
391+ segmentRecords . some (
392+ ( record ) => record . payload . requestKind === 'segment-prefetch'
393+ )
394+ ) . toBe ( true )
395+ expect (
396+ replayedSegmentRecords . some ( ( record ) =>
397+ getPersistedSegmentRequestKey ( record ) ?. endsWith ( '/__PAGE__' )
398+ )
399+ ) . toBe ( true )
400+ expect (
401+ replayedSegmentRecords . some ( ( record ) =>
402+ isPersistedHeadSegmentRecord ( record )
403+ )
404+ ) . toBe ( true )
405+ } )
406+ }
407+
356408 async function cleanupOfflineNavigationState (
357409 browser : Awaited < ReturnType < typeof next . browser > >
358410 ) {
@@ -1374,6 +1426,131 @@ describe('offlineNavigations build artifacts', () => {
13741426 }
13751427 } )
13761428
1429+ it ( 'replays a dynamic route from persisted known route patterns' , async ( ) => {
1430+ const buildResult = await next . build ( )
1431+ expect ( buildResult . exitCode ) . toBe ( 0 )
1432+
1433+ await next . start ( { skipBuild : true } )
1434+
1435+ let page : Playwright . Page | undefined
1436+ try {
1437+ const browser = await next . browser ( '/docs' , {
1438+ beforePageLoad ( p : Playwright . Page ) {
1439+ page = p
1440+ } ,
1441+ } )
1442+ await waitForOfflineNavigationServiceWorker ( browser , page ! )
1443+
1444+ await prefetchDynamicPatternReplayData ( browser )
1445+ await retry ( async ( ) => {
1446+ const routeRecords =
1447+ await readPersistedOfflineNavigationRouteRecords ( browser )
1448+ expect (
1449+ routeRecords . some ( ( record ) =>
1450+ record . route . pathname . includes ( '/dynamic-prefetch/replayed' )
1451+ )
1452+ ) . toBe ( false )
1453+ } )
1454+ await retry ( async ( ) => {
1455+ const cacheState = await readOfflineNavigationCacheState ( browser )
1456+ expect ( cacheState . entries ) . toEqual (
1457+ expect . arrayContaining ( [
1458+ {
1459+ cacheName : expect . stringMatching ( / ^ n e x t - o f f l i n e - n a v i g a t i o n - v 1 : / ) ,
1460+ pathname : expect . stringMatching (
1461+ / ^ \/ a p p - a s s e t s \/ _ n e x t \/ s t a t i c \/ (?: i m m u t a b l e \/ ) ? c h u n k s \/ .+ \. j s $ /
1462+ ) ,
1463+ } ,
1464+ ] )
1465+ )
1466+ } )
1467+
1468+ const session = await (
1469+ page ! . context ( ) as Playwright . BrowserContext & {
1470+ newCDPSession : ( page : Playwright . Page ) => Promise < {
1471+ send : ( method : string ) => Promise < void >
1472+ detach : ( ) => Promise < void >
1473+ } >
1474+ }
1475+ ) . newCDPSession ( page ! )
1476+
1477+ try {
1478+ await session . send ( 'Network.clearBrowserCache' )
1479+ } finally {
1480+ await session . detach ( )
1481+ }
1482+
1483+ await next . stop ( )
1484+ await page ! . context ( ) . setOffline ( true )
1485+ const dynamicReplayResponse = await page ! . goto (
1486+ `${ next . url } /docs/dynamic-prefetch/replayed#restored` ,
1487+ { waitUntil : 'domcontentloaded' }
1488+ )
1489+ expect ( dynamicReplayResponse ?. status ( ) ) . toBe ( 200 )
1490+ await retry ( async ( ) => {
1491+ expect ( await browser . elementById ( 'dynamic-prefetch-page' ) . text ( ) ) . toBe (
1492+ 'dynamic prefetch path: replayed'
1493+ )
1494+ } )
1495+ } finally {
1496+ if ( page ) {
1497+ await page . context ( ) . setOffline ( false )
1498+ }
1499+ await next . stop ( )
1500+ }
1501+ } )
1502+
1503+ it ( 'misses dynamic route pattern replay when a required segment record is missing' , async ( ) => {
1504+ const buildResult = await next . build ( )
1505+ expect ( buildResult . exitCode ) . toBe ( 0 )
1506+
1507+ await next . start ( { skipBuild : true } )
1508+
1509+ let page : Playwright . Page | undefined
1510+ try {
1511+ const browser = await next . browser ( '/docs' , {
1512+ beforePageLoad ( p : Playwright . Page ) {
1513+ page = p
1514+ } ,
1515+ } )
1516+ await waitForOfflineNavigationServiceWorker ( browser , page ! )
1517+
1518+ await prefetchDynamicPatternReplayData ( browser )
1519+
1520+ const deletedSegments =
1521+ await deletePersistedOfflineNavigationSegmentRecords ( browser , {
1522+ keySubstring : 'replayed' ,
1523+ requestKeySuffix : '/__PAGE__' ,
1524+ } )
1525+ expect ( deletedSegments ) . toBeGreaterThan ( 0 )
1526+ await retry ( async ( ) => {
1527+ const segmentRecords =
1528+ await readPersistedOfflineNavigationSegmentRecords ( browser )
1529+ expect (
1530+ segmentRecords . some (
1531+ ( record ) =>
1532+ record . key . includes ( 'replayed' ) &&
1533+ getPersistedSegmentRequestKey ( record ) ?. endsWith ( '/__PAGE__' )
1534+ )
1535+ ) . toBe ( false )
1536+ } )
1537+
1538+ await next . stop ( )
1539+ await page ! . context ( ) . setOffline ( true )
1540+ const missingSegmentResponse = await page ! . goto (
1541+ `${ next . url } /docs/dynamic-prefetch/replayed#missing-segment` ,
1542+ { waitUntil : 'domcontentloaded' }
1543+ )
1544+ expect ( missingSegmentResponse ?. status ( ) ) . toBe ( 200 )
1545+ await expectOfflineNavigationCacheMiss ( browser , 'missing-segment' )
1546+ } finally {
1547+ if ( page ) {
1548+ await page . context ( ) . setOffline ( false )
1549+ }
1550+ await next . stop ( )
1551+ }
1552+ } )
1553+
13771554 it ( 'does not emit offline navigation artifacts when disabled' , async ( ) => {
13781555 await next . patchFile ( 'next.config.js' , ( content ) =>
13791556 content . replace ( 'offlineNavigations: true' , 'offlineNavigations: false' )
0 commit comments