@@ -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 ) {
@@ -1368,6 +1420,131 @@ describe('offlineNavigations build artifacts', () => {
13681420 }
13691421 } )
13701422
1423+ it ( 'replays a dynamic route from persisted known route patterns' , async ( ) => {
1424+ const buildResult = await next . build ( )
1425+ expect ( buildResult . exitCode ) . toBe ( 0 )
1426+
1427+ await next . start ( { skipBuild : true } )
1428+
1429+ let page : Playwright . Page | undefined
1430+ try {
1431+ const browser = await next . browser ( '/docs' , {
1432+ beforePageLoad ( p : Playwright . Page ) {
1433+ page = p
1434+ } ,
1435+ } )
1436+ await waitForOfflineNavigationServiceWorker ( browser , page ! )
1437+
1438+ await prefetchDynamicPatternReplayData ( browser )
1439+ await retry ( async ( ) => {
1440+ const routeRecords =
1441+ await readPersistedOfflineNavigationRouteRecords ( browser )
1442+ expect (
1443+ routeRecords . some ( ( record ) =>
1444+ record . route . pathname . includes ( '/dynamic-prefetch/replayed' )
1445+ )
1446+ ) . toBe ( false )
1447+ } )
1448+ await retry ( async ( ) => {
1449+ const cacheState = await readOfflineNavigationCacheState ( browser )
1450+ expect ( cacheState . entries ) . toEqual (
1451+ expect . arrayContaining ( [
1452+ {
1453+ 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 : / ) ,
1454+ pathname : expect . stringMatching (
1455+ / ^ \/ 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 $ /
1456+ ) ,
1457+ } ,
1458+ ] )
1459+ )
1460+ } )
1461+
1462+ const session = await (
1463+ page ! . context ( ) as Playwright . BrowserContext & {
1464+ newCDPSession : ( page : Playwright . Page ) => Promise < {
1465+ send : ( method : string ) => Promise < void >
1466+ detach : ( ) => Promise < void >
1467+ } >
1468+ }
1469+ ) . newCDPSession ( page ! )
1470+
1471+ try {
1472+ await session . send ( 'Network.clearBrowserCache' )
1473+ } finally {
1474+ await session . detach ( )
1475+ }
1476+
1477+ await next . stop ( )
1478+ await page ! . context ( ) . setOffline ( true )
1479+ const dynamicReplayResponse = await page ! . goto (
1480+ `${ next . url } /docs/dynamic-prefetch/replayed#restored` ,
1481+ { waitUntil : 'domcontentloaded' }
1482+ )
1483+ expect ( dynamicReplayResponse ?. status ( ) ) . toBe ( 200 )
1484+ await retry ( async ( ) => {
1485+ expect ( await browser . elementById ( 'dynamic-prefetch-page' ) . text ( ) ) . toBe (
1486+ 'dynamic prefetch path: replayed'
1487+ )
1488+ } )
1489+ } finally {
1490+ if ( page ) {
1491+ await page . context ( ) . setOffline ( false )
1492+ }
1493+ await next . stop ( )
1494+ }
1495+ } )
1496+
1497+ it ( 'misses dynamic route pattern replay when a required segment record is missing' , async ( ) => {
1498+ const buildResult = await next . build ( )
1499+ expect ( buildResult . exitCode ) . toBe ( 0 )
1500+
1501+ await next . start ( { skipBuild : true } )
1502+
1503+ let page : Playwright . Page | undefined
1504+ try {
1505+ const browser = await next . browser ( '/docs' , {
1506+ beforePageLoad ( p : Playwright . Page ) {
1507+ page = p
1508+ } ,
1509+ } )
1510+ await waitForOfflineNavigationServiceWorker ( browser , page ! )
1511+
1512+ await prefetchDynamicPatternReplayData ( browser )
1513+
1514+ const deletedSegments =
1515+ await deletePersistedOfflineNavigationSegmentRecords ( browser , {
1516+ keySubstring : 'replayed' ,
1517+ requestKeySuffix : '/__PAGE__' ,
1518+ } )
1519+ expect ( deletedSegments ) . toBeGreaterThan ( 0 )
1520+ await retry ( async ( ) => {
1521+ const segmentRecords =
1522+ await readPersistedOfflineNavigationSegmentRecords ( browser )
1523+ expect (
1524+ segmentRecords . some (
1525+ ( record ) =>
1526+ record . key . includes ( 'replayed' ) &&
1527+ getPersistedSegmentRequestKey ( record ) ?. endsWith ( '/__PAGE__' )
1528+ )
1529+ ) . toBe ( false )
1530+ } )
1531+
1532+ await next . stop ( )
1533+ await page ! . context ( ) . setOffline ( true )
1534+ const missingSegmentResponse = await page ! . goto (
1535+ `${ next . url } /docs/dynamic-prefetch/replayed#missing-segment` ,
1536+ { waitUntil : 'domcontentloaded' }
1537+ )
1538+ expect ( missingSegmentResponse ?. status ( ) ) . toBe ( 200 )
1539+ await expectOfflineNavigationCacheMiss ( browser , 'missing-segment' )
1540+ } finally {
1541+ if ( page ) {
1542+ await page . context ( ) . setOffline ( false )
1543+ }
1544+ await next . stop ( )
1545+ }
1546+ } )
1547+
13711548 it ( 'does not emit offline navigation artifacts when disabled' , async ( ) => {
13721549 await next . patchFile ( 'next.config.js' , ( content ) =>
13731550 content . replace ( 'offlineNavigations: true' , 'offlineNavigations: false' )
0 commit comments