@@ -331,6 +331,58 @@ describe('offlineNavigations build artifacts', () => {
331331 } )
332332 }
333333
334+ async function prefetchDynamicPatternReplayData (
335+ browser : Awaited < ReturnType < typeof next . browser > >
336+ ) {
337+ await browser . elementById ( 'prefetch-dynamic-pattern-source' ) . click ( )
338+ await retry ( async ( ) => {
339+ const routeRecords =
340+ await readPersistedOfflineNavigationRouteRecords ( browser )
341+ expect (
342+ routeRecords . some ( ( record ) =>
343+ record . route . pathname . includes ( '/dynamic-prefetch/learned' )
344+ )
345+ ) . toBe ( true )
346+ } )
347+
348+ await browser . elementById ( 'prefetch-dynamic-pattern-target' ) . click ( )
349+ await retry ( async ( ) => {
350+ const routeRecords =
351+ await readPersistedOfflineNavigationRouteRecords ( browser )
352+ expect (
353+ routeRecords . some ( ( record ) =>
354+ record . route . pathname . includes ( '/dynamic-prefetch/learned' )
355+ )
356+ ) . toBe ( true )
357+ expect (
358+ routeRecords . some ( ( record ) =>
359+ record . route . pathname . includes ( '/dynamic-prefetch/replayed' )
360+ )
361+ ) . toBe ( false )
362+
363+ const segmentRecords =
364+ await readPersistedOfflineNavigationSegmentRecords ( browser )
365+ const replayedSegmentRecords = segmentRecords . filter ( ( record ) =>
366+ record . key . includes ( 'replayed' )
367+ )
368+ expect (
369+ segmentRecords . some (
370+ ( record ) => record . payload . requestKind === 'segment-prefetch'
371+ )
372+ ) . toBe ( true )
373+ expect (
374+ replayedSegmentRecords . some ( ( record ) =>
375+ record . segment . requestKey . endsWith ( '/__PAGE__' )
376+ )
377+ ) . toBe ( true )
378+ expect (
379+ replayedSegmentRecords . some (
380+ ( record ) => record . segment . requestKey === '/_head'
381+ )
382+ ) . toBe ( true )
383+ } )
384+ }
385+
334386 async function cleanupOfflineNavigationState (
335387 browser : Awaited < ReturnType < typeof next . browser > >
336388 ) {
@@ -1357,6 +1409,131 @@ describe('offlineNavigations build artifacts', () => {
13571409 }
13581410 } )
13591411
1412+ it ( 'replays a dynamic route from persisted known route patterns' , async ( ) => {
1413+ const buildResult = await next . build ( )
1414+ expect ( buildResult . exitCode ) . toBe ( 0 )
1415+
1416+ await next . start ( { skipBuild : true } )
1417+
1418+ let page : Playwright . Page | undefined
1419+ try {
1420+ const browser = await next . browser ( '/docs' , {
1421+ beforePageLoad ( p : Playwright . Page ) {
1422+ page = p
1423+ } ,
1424+ } )
1425+ await waitForOfflineNavigationServiceWorker ( browser , page ! )
1426+
1427+ await prefetchDynamicPatternReplayData ( browser )
1428+ await retry ( async ( ) => {
1429+ const routeRecords =
1430+ await readPersistedOfflineNavigationRouteRecords ( browser )
1431+ expect (
1432+ routeRecords . some ( ( record ) =>
1433+ record . route . pathname . includes ( '/dynamic-prefetch/replayed' )
1434+ )
1435+ ) . toBe ( false )
1436+ } )
1437+ await retry ( async ( ) => {
1438+ const cacheState = await readOfflineNavigationCacheState ( browser )
1439+ expect ( cacheState . entries ) . toEqual (
1440+ expect . arrayContaining ( [
1441+ {
1442+ 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 : / ) ,
1443+ pathname : expect . stringMatching (
1444+ / ^ \/ 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 $ /
1445+ ) ,
1446+ } ,
1447+ ] )
1448+ )
1449+ } )
1450+
1451+ const session = await (
1452+ page ! . context ( ) as Playwright . BrowserContext & {
1453+ newCDPSession : ( page : Playwright . Page ) => Promise < {
1454+ send : ( method : string ) => Promise < void >
1455+ detach : ( ) => Promise < void >
1456+ } >
1457+ }
1458+ ) . newCDPSession ( page ! )
1459+
1460+ try {
1461+ await session . send ( 'Network.clearBrowserCache' )
1462+ } finally {
1463+ await session . detach ( )
1464+ }
1465+
1466+ await next . stop ( )
1467+ await page ! . context ( ) . setOffline ( true )
1468+ const dynamicReplayResponse = await page ! . goto (
1469+ `${ next . url } /docs/dynamic-prefetch/replayed#restored` ,
1470+ { waitUntil : 'domcontentloaded' }
1471+ )
1472+ expect ( dynamicReplayResponse ?. status ( ) ) . toBe ( 200 )
1473+ await retry ( async ( ) => {
1474+ expect ( await browser . elementById ( 'dynamic-prefetch-page' ) . text ( ) ) . toBe (
1475+ 'dynamic prefetch path: replayed'
1476+ )
1477+ } )
1478+ } finally {
1479+ if ( page ) {
1480+ await page . context ( ) . setOffline ( false )
1481+ }
1482+ await next . stop ( )
1483+ }
1484+ } )
1485+
1486+ it ( 'misses dynamic route pattern replay when a required segment record is missing' , async ( ) => {
1487+ const buildResult = await next . build ( )
1488+ expect ( buildResult . exitCode ) . toBe ( 0 )
1489+
1490+ await next . start ( { skipBuild : true } )
1491+
1492+ let page : Playwright . Page | undefined
1493+ try {
1494+ const browser = await next . browser ( '/docs' , {
1495+ beforePageLoad ( p : Playwright . Page ) {
1496+ page = p
1497+ } ,
1498+ } )
1499+ await waitForOfflineNavigationServiceWorker ( browser , page ! )
1500+
1501+ await prefetchDynamicPatternReplayData ( browser )
1502+
1503+ const deletedSegments =
1504+ await deletePersistedOfflineNavigationSegmentRecords ( browser , {
1505+ keySubstring : 'replayed' ,
1506+ requestKeySuffix : '/__PAGE__' ,
1507+ } )
1508+ expect ( deletedSegments ) . toBeGreaterThan ( 0 )
1509+ await retry ( async ( ) => {
1510+ const segmentRecords =
1511+ await readPersistedOfflineNavigationSegmentRecords ( browser )
1512+ expect (
1513+ segmentRecords . some (
1514+ ( record ) =>
1515+ record . key . includes ( 'replayed' ) &&
1516+ record . segment . requestKey . endsWith ( '/__PAGE__' )
1517+ )
1518+ ) . toBe ( false )
1519+ } )
1520+
1521+ await next . stop ( )
1522+ await page ! . context ( ) . setOffline ( true )
1523+ const missingSegmentResponse = await page ! . goto (
1524+ `${ next . url } /docs/dynamic-prefetch/replayed#missing-segment` ,
1525+ { waitUntil : 'domcontentloaded' }
1526+ )
1527+ expect ( missingSegmentResponse ?. status ( ) ) . toBe ( 200 )
1528+ await expectOfflineNavigationCacheMiss ( browser , 'missing-segment' )
1529+ } finally {
1530+ if ( page ) {
1531+ await page . context ( ) . setOffline ( false )
1532+ }
1533+ await next . stop ( )
1534+ }
1535+ } )
1536+
13601537 it ( 'does not emit offline navigation artifacts when disabled' , async ( ) => {
13611538 await next . patchFile ( 'next.config.js' , ( content ) =>
13621539 content . replace ( 'offlineNavigations: true' , 'offlineNavigations: false' )
0 commit comments