import { nextTestSetup } from 'e2e-utils' import type * as Playwright from 'playwright' import { createRouterAct } from 'router-act' describe('segment cache prefetch scheduling', () => { const { next, isNextDev } = nextTestSetup({ files: __dirname, }) if (isNextDev) { test('prefetching is disabled', () => {}) return } it('increases the priority of a viewport-initiated prefetch on hover', async () => { let act: ReturnType const browser = await next.browser('/cancellation', { beforePageLoad(p: Playwright.Page) { act = createRouterAct(p) }, }) const checkbox = await browser.elementByCss('input[type="checkbox"]') await act( async () => { // Reveal the links to start prefetching, but block the responses from // reaching the client. This will initiate prefetches for the route // trees, but it won't start prefetching any segment data yet until the // trees have loaded. await act(async () => { await checkbox.click() }, 'block') // Hover over a link to increase its relative priority. const link2 = await browser.elementByCss('a[href="/cancellation/2"]') await link2.hover() // Hover over a different link to increase its relative priority. const link5 = await browser.elementByCss('a[href="/cancellation/5"]') await link5.hover() }, // Assert that the segment data is prefetched in the expected order. [ // The last link we hovered over should be the first to prefetch. { includes: 'Content of page 5' }, // The second-to-last link we hovered over should come next. { includes: 'Content of page 2' }, // Then all the other links come after that. (We don't need to assert // on every single prefetch response. I picked one of them arbitrarily.) { includes: 'Content of page 4' }, ] ) }) // TODO: This test no longer works as-written because the metadata is now // fetched separately from the route tree. Need to rewrite to assert on // something that appears in the route tree and is relatively stable, like a // static route name. it.skip('prioritizes prefetching the route trees before the segments', async () => { let act: ReturnType const browser = await next.browser('/cancellation', { beforePageLoad(p: Playwright.Page) { act = createRouterAct(p) }, }) const checkbox = await browser.elementByCss('input[type="checkbox"]') await act(async () => { // Reveal the links to start prefetching await checkbox.click() }, [ // Assert on the order that the prefetches requests are // initiated. We don't need to assert on every single prefetch response; // this will only check the order of the ones that we've listed. // // To detect when the route tree is prefetched, we check for a string // that is known to be present in the target page's viewport config // (which is included in the route tree response). In this test app, the // page number is used in the media query of the theme color. E.g. for // page 1, the viewport includes: // // // First we should prefetch all the route trees: { includes: '(min-width: 7px)' }, { includes: '(min-width: 6px)' }, { includes: '(min-width: 5px)' }, { includes: '(min-width: 4px)' }, { includes: '(min-width: 3px)' }, // Then we should prefetch the segments: { includes: 'Content of page 7' }, { includes: 'Content of page 6' }, { includes: 'Content of page 5' }, { includes: 'Content of page 4' }, { includes: 'Content of page 3' }, ]) }) it('reserve special bandwidth for the most recently hovered link', async () => { let act: ReturnType const browser = await next.browser('/cancellation', { beforePageLoad(p: Playwright.Page) { act = createRouterAct(p) }, }) const checkbox = await browser.elementByCss('input[type="checkbox"]') await act(async () => { // Reveal the links to start prefetching, but block the responses from // reaching the client. This will initiate prefetches for the route // trees, but it won't start prefetching any segment data yet until the // trees have loaded. await act(async () => { await checkbox.click() }, 'block') // At this point, the network queue is already at max capacity, so // revealing more links won't initiate any new requests. await act(async () => { const showMoreLinksButton = await browser.elementById('show-more-links') await showMoreLinksButton.click() }, 'no-requests') // However, when hovering over a link, the prefetch task is allowed to // exceed the default limit. So hovering over a link does initiate a // new request. await act(async () => { // Hover over a link. This will initiate a request for the route tree, // but before we're able to fetch the segments, we'll have already // hovered over a different link. const link2 = await browser.elementByCss('a[href="/cancellation/2"]') await link2.hover() // Immediately hover over a different link. const link3 = await browser.elementByCss('a[href="/cancellation/3"]') await link3.hover() }, [ // The most recently hovered link is allowed to finish loading its // segment data. { includes: 'Content of page 3' }, // The previously hovered link was downgraded to the default priority, // so it should still be blocked. { includes: 'Content of page 2', block: 'reject' }, ]) }, [ // Assert that everything else proceeds in the expected order. We don't // need to assert on every single prefetch response; I picked just a few // of them. // The previously hovered link is the next to finish loading, because // even though it was downgraded to the default priority, it was still // moved ahead of the other default tasks. { includes: 'Content of page 2' }, // Next are the links that were revealed by the "Show More" button. { includes: 'Content of page 8' }, // Then the rest. { includes: 'Content of page 5' }, ]) }) it( 'cancels a viewport-initiated prefetch if the link leaves the viewport ' + 'before it finishes', async () => { let act: ReturnType const browser = await next.browser('/cancellation', { beforePageLoad(p: Playwright.Page) { act = createRouterAct(p) }, }) const checkbox = await browser.elementByCss('input[type="checkbox"]') await act( async () => { // Reveal the links to start prefetching, but block the responses from // reaching the client. Because the router limits the number of // concurrent prefetches, not all the links will start prefetching — // some of them will remain in the queue, waiting for additional // network bandwidth. This test demonstrates that those prefetches // will be canceled on viewport exit, too. await act(async () => { await checkbox.click() }, 'block') // Before the prefetch finishes, click the checkbox again to hide // the link. await checkbox.click() }, // When the outer `act` scope finishes, the route tree prefetch will // continue. Normally when the router is done prefetching the route // tree, it will proceed to prefetching the segments. However, since // the link is no longer visible, it should stop prefetching. // // Assert that no additional network requests are initiated in this // outer scope. If this fails, it suggests that the prefetches were not // canceled when the links left the viewport. 'no-requests' ) } ) it("reschedules a link's prefetch when it re-enters the viewport", async () => { let act: ReturnType const browser = await next.browser('/cancellation', { beforePageLoad(p: Playwright.Page) { act = createRouterAct(p) }, }) const checkbox = await browser.elementByCss('input[type="checkbox"]') await act( async () => { // Reveal the links to start prefetching, but block the responses from // reaching the client. Because the router limits the number of // concurrent prefetches, not all the links will start prefetching — // some of them will remain in the queue, waiting for additional // network bandwidth. This test demonstrates that those prefetches // will be canceled on viewport exit, too. await act(async () => { await checkbox.click() }, 'block') // Before the prefetch finishes, click the checkbox again to hide // the link. await checkbox.click() }, // When the outer `act` scope finishes, the route tree prefetch will // continue. Normally when the router is done prefetching the route // tree, it will proceed to prefetching the segments. However, since // the link is no longer visible, it should stop prefetching. // // Assert that no additional network requests are initiated in this // outer scope. If this fails, it suggests that the prefetches were not // canceled when the links left the viewport. 'no-requests' ) // Now we'll reveal the links again to verify that the prefetch tasks are // rescheduled, after having been canceled. await act( async () => { await checkbox.click() }, // Don't need to assert on all the prefetch responses. I picked an // arbitrary one. { includes: 'Content of page 5' } ) }) })