diff --git a/test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts b/test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts index be510febeae71..44929fc0daa7a 100644 --- a/test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts +++ b/test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts @@ -1,5 +1,5 @@ import { FileRef, nextTestSetup } from 'e2e-utils' -import { createRouterAct } from './router-act' +import { createRouterAct } from 'e2e-utils/router-act' import { createTimeController } from './test-utils' import { join } from 'path' diff --git a/test/e2e/app-dir/app-prefetch/prefetching.test.ts b/test/e2e/app-dir/app-prefetch/prefetching.test.ts index 4c9492d98dff7..a870f75c51a6c 100644 --- a/test/e2e/app-dir/app-prefetch/prefetching.test.ts +++ b/test/e2e/app-dir/app-prefetch/prefetching.test.ts @@ -1,8 +1,8 @@ import { FileRef, nextTestSetup } from 'e2e-utils' +import { createRouterAct } from 'e2e-utils/router-act' import { waitFor, retry } from 'next-test-utils' import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers' import { computeCacheBustingSearchParam } from 'next/dist/shared/lib/router/utils/cache-busting-search-param' -import { createRouterAct } from './router-act' import { createTimeController } from './test-utils' import { join } from 'path' diff --git a/test/e2e/app-dir/app-prefetch/router-act.ts b/test/e2e/app-dir/app-prefetch/router-act.ts deleted file mode 100644 index d73e4cfdb8277..0000000000000 --- a/test/e2e/app-dir/app-prefetch/router-act.ts +++ /dev/null @@ -1,504 +0,0 @@ -import type * as Playwright from 'playwright' -import { diff } from 'jest-diff' -import { equals } from '@jest/expect-utils' - -type Batch = { - pendingRequestChecks: Set> - pendingRequests: Set -} - -type PendingRSCRequest = { - url: string - route: Playwright.Route | null - result: Promise<{ - text: string - body: any - headers: Record - status: number - }> - didProcess: boolean -} - -let currentBatch: Batch | null = null - -type ExpectedResponseConfig = { includes: string; block?: boolean | 'reject' } - -/** - * Represents the expected responses sent by the server to fulfill requests - * initiated by the `scope` function. - * - * - `includes` is a substring of an expected response body. - * - `block` indicates whether the response should not yet be sent to the - * client. This option is only supported when nested inside an outer `act` - * scope. The blocked response will be fulfilled when the outer - * scope completes. - * - * The list of expected responses does not need to be exhaustive — any - * responses that don't match will proceed like normal. However, `act` will - * error if the expected substring is not found in any of the responses, or - * if the expected responses are received out of order. It will also error - * if the same expected substring is found in multiple responses. - * - * If no expected responses are provided, the only expectation is that at - * least one request is initiated. (This is the same as passing an - * empty array.) - * - * Alternatively, if no network activity is expected, pass "no-requests". - */ -type ActConfig = - | ExpectedResponseConfig - | Array - | 'block' - | 'no-requests' - | null - -export function createRouterAct( - page: Playwright.Page -): (scope: () => Promise | T, config?: ActConfig) => Promise { - /** - * Test utility for requests initiated by the Next.js Router, such as - * prefetches and navigations. Calls the given async function then intercepts - * any router requests that are initiated as a result. It will then wait for - * all the requests to complete before exiting. Inspired by the React - * `act` API. - */ - async function act( - scope: () => Promise | T, - config?: ActConfig - ): Promise { - // Capture a stack trace for better async error messages. - const error = new Error() - if (Error.captureStackTrace) { - Error.captureStackTrace(error, act) - } - - let expectedResponses: Array | null - let forbiddenResponses: Array | null = null - let shouldBlockAll = false - if (config === undefined || config === null) { - // Default. Expect at least one request, but don't assert on the response. - expectedResponses = [] - } else if (config === 'block') { - // Expect at least one request, and block them all from being fulfilled. - if (currentBatch === null) { - error.message = - '`block` option only supported when nested inside an outer ' + - '`act` scope.' - throw error - } - expectedResponses = [] - shouldBlockAll = true - } else if (config === 'no-requests') { - // Expect no requests to be initiated. - expectedResponses = null - } else if (!Array.isArray(config)) { - // Shortcut for a single expected response. - if (config.block === true && currentBatch === null) { - error.message = - '`block: true` option only supported when nested inside an outer ' + - '`act` scope.' - throw error - } - if (config.block !== 'reject') { - expectedResponses = [config] - } else { - expectedResponses = [] - forbiddenResponses = [config] - } - } else { - expectedResponses = [] - for (const item of config) { - if (item.block === true && currentBatch === null) { - error.message = - '`block: true` option only supported when nested inside an outer ' + - '`act` scope.' - throw error - } - if (item.block !== 'reject') { - expectedResponses.push(item) - } else { - if (forbiddenResponses === null) { - forbiddenResponses = [item] - } else { - forbiddenResponses.push(item) - } - } - } - } - - // Attach a route handler to intercept router requests for the duration - // of the `act` scope. It will be removed before `act` exits. - let onDidIssueFirstRequest: (() => void) | null = null - const routeHandler = async (route: Playwright.Route) => { - const request = route.request() - - const pendingRequests = batch.pendingRequests - const pendingRequestChecks = batch.pendingRequestChecks - - // Because determining whether we need to intercept the request is an - // async operation, we collect these promises so we can await them at the - // end of the `act` scope to see whether any additional requests - // were initiated. - // NOTE: The default check doesn't actually need to be async, but since - // this logic is subtle, to preserve the ability to add an async - // check later, I'm treating it as if it could possibly be async. - const checkIfRouterRequest = (async () => { - const headers = request.headers() - - // The default check includes navigations, prefetches, and actions. - const isRouterRequest = - headers['rsc'] !== undefined || // Matches navigations and prefetches - headers['next-action'] !== undefined // Matches Server Actions - - if (isRouterRequest) { - // This request was initiated by the Next.js Router. Intercept it and - // add it to the current batch. - pendingRequests.add({ - url: request.url(), - route, - // `act` controls the timing of when responses reach the client, - // but it should not affect the timing of when requests reach the - // server; we pass the request to the server the immediately. - result: new Promise(async (resolve) => { - const originalResponse = await page.request.fetch(request, { - maxRedirects: 0, - }) - - // WORKAROUND: - // intercepting responses with 'Transfer-Encoding: chunked' (used for streaming) - // seems to be problematic sometimes, making the browser error with `net::ERR_INCOMPLETE_CHUNKED_ENCODING`. - // In particular, this seems to happen when blocking a streaming navigation response. (but not always) - // Playwright buffers the whole body anyway, so we can remove the header to sidestep this. - const headers = originalResponse.headers() - delete headers['transfer-encoding'] - - resolve({ - text: await originalResponse.text(), - body: await originalResponse.body(), - headers, - status: originalResponse.status(), - }) - }), - didProcess: false, - }) - if (onDidIssueFirstRequest !== null) { - onDidIssueFirstRequest() - onDidIssueFirstRequest = null - } - return - } - // This is some other request not related to the Next.js Router. Allow - // it to continue as normal. - route.continue() - })() - - pendingRequestChecks.add(checkIfRouterRequest) - await checkIfRouterRequest - // Once we've read the header, we can remove it from the pending set. - pendingRequestChecks.delete(checkIfRouterRequest) - } - - let didHardNavigate = false - const hardNavigationHandler = async () => { - // If a hard navigation occurs, the current batch of requests is no longer - // valid. In fact, Playwright will hang indefinitely if we attempt to - // await the response of an orphaned request. Reset the batch and unblock - // all the orphaned requests. - const orphanedRequests = batch.pendingRequests - batch.pendingRequests = new Set() - batch.pendingRequestChecks = new Set() - await Promise.all( - Array.from(orphanedRequests).map((item) => item.route.continue()) - ) - didHardNavigate = true - } - - const waitForPendingRequestChecks = async () => { - const prevChecks = batch.pendingRequestChecks - batch.pendingRequestChecks = new Set() - await Promise.all(prevChecks) - } - - const prevBatch = currentBatch - const batch: Batch = { - pendingRequestChecks: new Set(), - pendingRequests: new Set(), - } - currentBatch = batch - await page.route('**/*', routeHandler) - await page.on('framedetached', hardNavigationHandler) - try { - // Call the user-provided scope function - const returnValue = await scope() - - // Wait until the first request is initiated, up to some timeout. - if (expectedResponses !== null && batch.pendingRequests.size === 0) { - await new Promise((resolve, reject) => { - const timerId = setTimeout(() => { - error.message = 'Timed out waiting for a request to be initiated.' - reject(error) - }, 500) - onDidIssueFirstRequest = () => { - clearTimeout(timerId) - resolve() - } - }) - } - - // Fulfill all the requests that were initiated by the scope function. But - // first, wait an additional browser task. This simulates the real world - // behavior where the network response is received in an async event/task - // that comes after the scope function, rather than immediately when the - // scope function exits. - // - // We use requestIdleCallback to schedule the task because that's - // guaranteed to fire after any IntersectionObserver events, which the - // router uses to track the visibility of links. - await page.evaluate( - () => new Promise((res) => requestIdleCallback(() => res())) - ) - - // Checking whether a request needs to be intercepted is an async - // operation, so we need to wait for all the checks to complete before - // checking whether the queue is empty. - await waitForPendingRequestChecks() - - // Because responding to one request may unblock additional requests, - // keep checking for more requests until the queue has settled. - const remaining = new Set() - let actualResponses: Array = [] - let alreadyMatched = new Map() - while (batch.pendingRequests.size > 0) { - const pending = batch.pendingRequests - batch.pendingRequests = new Set() - for (const item of pending) { - const route = item.route - const url = item.url - - let shouldBlock = false - const fulfilled = await item.result - if (item.didProcess) { - // This response was already processed by an inner `act` call. - } else { - item.didProcess = true - if (expectedResponses === null) { - error.message = ` -Expected no network requests to be initiated. - -URL: ${url} -Headers: ${JSON.stringify(fulfilled.headers)} - -Response: -${fulfilled.body} -` - - throw error - } - if (fulfilled.status >= 400) { - error.message = ` -Received a response with an error status code. - -Status: ${fulfilled.status} -URL: ${url} -Headers: ${JSON.stringify(fulfilled.headers)} - -Response: -${fulfilled.body} -` - throw error - } - if (forbiddenResponses !== null) { - for (const forbiddenResponse of forbiddenResponses) { - const includes = forbiddenResponse.includes - if (fulfilled.body.includes(includes)) { - error.message = ` -Received a response containing an unexpected substring: - -Rejected substring: ${includes} - -Response: -${fulfilled.body} -` - throw error - } - } - } - if (expectedResponses !== null) { - for (const expectedResponse of expectedResponses) { - const includes = expectedResponse.includes - const block = expectedResponse.block - if (fulfilled.body.includes(includes)) { - // Match. Don't check yet whether the responses are received - // in the expected order. Instead collect all the matches and - // check at the end so we can include a diff in the - // error message. - const otherResponse = alreadyMatched.get(includes) - if (otherResponse !== undefined) { - error.message = ` -Received multiple responses containing the same expected substring. - -Expected substring: -${includes} - -Responses: - -${otherResponse} - -${fulfilled.body} - -Choose a more specific substring to assert on. -` - throw error - } - alreadyMatched.set(includes, fulfilled.body) - if (actualResponses === null) { - actualResponses = [expectedResponse] - } else { - actualResponses.push(expectedResponse) - } - if (block) { - shouldBlock = true - } - // Keep checking all the expected responses to verify there - // are no duplicate matches - } - } - } - } - - if (shouldBlock || shouldBlockAll) { - // This response was blocked by the `block` option. Don't - // fulfill it yet. - remaining.add(item) - if (route === null) { - error.message = ` -The "block" option is not supported for requests that are redirected. - -URL: ${url} -Headers: ${JSON.stringify(fulfilled.headers)} - -Response: -${fulfilled.body} -` - - throw error - } - } else { - if (route !== null) { - const request = route.request() - await route.fulfill({ - body: fulfilled.body, - headers: fulfilled.headers, - status: fulfilled.status, - }) - const browserResponse = await request.response() - if (browserResponse !== null) { - await browserResponse.finished() - } - } - } - - if (fulfilled.status === 307 || fulfilled.status === 308) { - // When fulfilling a redirect, for some reason, the page.route() - // handler installed earlier will not intercept the - // redirect request. Install a one-off event listener to wait for - // the redirected request to finish. This works for this case - // because we don't need to modify to delay the response; we only - // need to observe when it has finished. - // TODO: Because this request cannot be intercepted, it's - // incompatible with the "block" option. I haven't yet figured out - // a strategy to make that work. In the meantime, attempting to - // write a test that blocks a redirect will result in an error - // (see error above). - await new Promise((resolve) => { - page.once('request', (req) => { - const handleResponse = (res: Playwright.Response) => { - if (res.url() === req.url()) { - batch.pendingRequests.add({ - url: req.url(), - route: null, - result: new Promise(async (resolve) => { - resolve({ - text: await res.text(), - body: await res.body(), - headers: res.headers(), - status: res.status(), - }) - }), - didProcess: false, - }) - page.off('response', handleResponse) - resolve() - } - } - page.on('response', handleResponse) - }) - }) - } - } - - // After flushing the queue, wait for the microtask queue to be - // exhausted, then check if any additional requests are initiated. A - // single macrotask should be enough because if the router queue is - // network throttled, the next request is issued either directly within - // the task of the previous request's completion event, or in the - // microtask queue of that event. - await page.evaluate( - () => new Promise((res) => requestIdleCallback(() => res())) - ) - - await waitForPendingRequestChecks() - } - - if (didHardNavigate) { - error.message = - 'A hard navigation or refresh was triggerd during the `act` scope. ' + - 'This is not supported.' - throw error - } - - if (expectedResponses !== null) { - // Assert that the responses were received in the expected order - if (!equals(actualResponses, expectedResponses)) { - // Print a helpful error message. - - if (expectedResponses.length === 1) { - error.message = - 'Expected a response containing the given string:\n\n' + - expectedResponses[0].includes + - '\n' - } else { - const expectedSubstrings = expectedResponses.map( - (item) => item.includes - ) - const actualSubstrings = actualResponses.map( - (item) => item.includes - ) - error.message = - 'Expected sequence of responses does not match:\n\n' + - diff(expectedSubstrings, actualSubstrings) + - '\n' - } - throw error - } - } - - // Some of the requests were blocked. Transfer them to the outer `act` - // batch so it can flush them. - if (remaining.size !== 0 && prevBatch !== null) { - for (const item of remaining) { - prevBatch.pendingRequests.add(item) - } - } - - return returnValue - } finally { - // Clean up - currentBatch = prevBatch - await page.unroute('**/*', routeHandler) - await page.off('framedetached', hardNavigationHandler) - } - } - - return act -} diff --git a/test/e2e/app-dir/segment-cache/basic/segment-cache-basic.test.ts b/test/e2e/app-dir/segment-cache/basic/segment-cache-basic.test.ts index dd1647292617e..873679b0ed365 100644 --- a/test/e2e/app-dir/segment-cache/basic/segment-cache-basic.test.ts +++ b/test/e2e/app-dir/segment-cache/basic/segment-cache-basic.test.ts @@ -1,5 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -import { createRouterAct } from '../router-act' +import { createRouterAct } from 'e2e-utils/router-act' import { waitFor } from 'next-test-utils' describe('segment cache (basic tests)', () => { diff --git a/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts b/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts index ff8070c83b1ca..c5d30482afb3c 100644 --- a/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts +++ b/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts @@ -1,8 +1,8 @@ import type * as Playwright from 'playwright' import webdriver from 'next-webdriver' -import { createRouterAct } from '../router-act' import { findPort, nextBuild } from 'next-test-utils' import { isNextDeploy, isNextDev } from 'e2e-utils' +import { createRouterAct } from 'e2e-utils/router-act' import { start } from './server.mjs' describe('segment cache (CDN cache busting)', () => { diff --git a/test/e2e/app-dir/segment-cache/client-only-opt-in/client-only-opt-in.test.ts b/test/e2e/app-dir/segment-cache/client-only-opt-in/client-only-opt-in.test.ts index 4ef9106b741c8..c9461f53f1eed 100644 --- a/test/e2e/app-dir/segment-cache/client-only-opt-in/client-only-opt-in.test.ts +++ b/test/e2e/app-dir/segment-cache/client-only-opt-in/client-only-opt-in.test.ts @@ -1,6 +1,6 @@ import { nextTestSetup } from 'e2e-utils' +import { createRouterAct } from 'e2e-utils/router-act' import type * as Playwright from 'playwright' -import { createRouterAct } from '../router-act' describe('segment cache prefetch scheduling', () => { const { next, isNextDev } = nextTestSetup({ diff --git a/test/e2e/app-dir/segment-cache/deployment-skew/deployment-skew.test.ts b/test/e2e/app-dir/segment-cache/deployment-skew/deployment-skew.test.ts index 881e122d4e278..968c9e37e7986 100644 --- a/test/e2e/app-dir/segment-cache/deployment-skew/deployment-skew.test.ts +++ b/test/e2e/app-dir/segment-cache/deployment-skew/deployment-skew.test.ts @@ -1,8 +1,8 @@ import type * as Playwright from 'playwright' import webdriver from 'next-webdriver' -import { createRouterAct } from '../router-act' import { findPort } from 'next-test-utils' import { isNextDeploy, isNextDev } from 'e2e-utils' +import { createRouterAct } from 'e2e-utils/router-act' import { build, start } from './servers.mjs' describe('segment cache (deployment skew)', () => { diff --git a/test/e2e/app-dir/segment-cache/dynamic-on-hover/dynamic-on-hover.test.ts b/test/e2e/app-dir/segment-cache/dynamic-on-hover/dynamic-on-hover.test.ts index 1b4c0b35248aa..a10180c82cfe8 100644 --- a/test/e2e/app-dir/segment-cache/dynamic-on-hover/dynamic-on-hover.test.ts +++ b/test/e2e/app-dir/segment-cache/dynamic-on-hover/dynamic-on-hover.test.ts @@ -1,6 +1,6 @@ import { nextTestSetup } from 'e2e-utils' +import { createRouterAct } from 'e2e-utils/router-act' import type * as Playwright from 'playwright' -import { createRouterAct } from '../router-act' describe('dynamic on hover', () => { const { next, isNextDev } = nextTestSetup({ diff --git a/test/e2e/app-dir/segment-cache/export/segment-cache-output-export.test.ts b/test/e2e/app-dir/segment-cache/export/segment-cache-output-export.test.ts index e813a25a8db56..44ce5373c6843 100644 --- a/test/e2e/app-dir/segment-cache/export/segment-cache-output-export.test.ts +++ b/test/e2e/app-dir/segment-cache/export/segment-cache-output-export.test.ts @@ -1,8 +1,8 @@ import type * as Playwright from 'playwright' import webdriver from 'next-webdriver' -import { createRouterAct } from '../router-act' import { findPort, nextBuild } from 'next-test-utils' import { isNextStart } from 'e2e-utils' +import { createRouterAct } from 'e2e-utils/router-act' import { server } from './server.mjs' describe('segment cache (output: "export")', () => { diff --git a/test/e2e/app-dir/segment-cache/incremental-opt-in/segment-cache-incremental-opt-in.test.ts b/test/e2e/app-dir/segment-cache/incremental-opt-in/segment-cache-incremental-opt-in.test.ts index 9b7e5cec17fc9..22b5b28340c48 100644 --- a/test/e2e/app-dir/segment-cache/incremental-opt-in/segment-cache-incremental-opt-in.test.ts +++ b/test/e2e/app-dir/segment-cache/incremental-opt-in/segment-cache-incremental-opt-in.test.ts @@ -1,5 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -import { createRouterAct } from '../router-act' +import { createRouterAct } from 'e2e-utils/router-act' import { Page } from 'playwright' describe('segment cache (incremental opt in)', () => { diff --git a/test/e2e/app-dir/segment-cache/memory-pressure/segment-cache-memory-pressure.test.ts b/test/e2e/app-dir/segment-cache/memory-pressure/segment-cache-memory-pressure.test.ts index bcb7a0f2fec50..cbec9327e97ee 100644 --- a/test/e2e/app-dir/segment-cache/memory-pressure/segment-cache-memory-pressure.test.ts +++ b/test/e2e/app-dir/segment-cache/memory-pressure/segment-cache-memory-pressure.test.ts @@ -1,5 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -import { createRouterAct } from '../router-act' +import { createRouterAct } from 'e2e-utils/router-act' describe('segment cache memory pressure', () => { const { next, isNextDev } = nextTestSetup({ diff --git a/test/e2e/app-dir/segment-cache/metadata/segment-cache-metadata.test.ts b/test/e2e/app-dir/segment-cache/metadata/segment-cache-metadata.test.ts index 2866e499a38ec..83f1be8547ff9 100644 --- a/test/e2e/app-dir/segment-cache/metadata/segment-cache-metadata.test.ts +++ b/test/e2e/app-dir/segment-cache/metadata/segment-cache-metadata.test.ts @@ -1,5 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -import { createRouterAct } from '../router-act' +import { createRouterAct } from 'e2e-utils/router-act' describe('segment cache (metadata)', () => { const { next, isNextDev } = nextTestSetup({ diff --git a/test/e2e/app-dir/segment-cache/no-prefetch/no-prefetch.test.ts b/test/e2e/app-dir/segment-cache/no-prefetch/no-prefetch.test.ts index 438372ab38a6a..048f0bd8dca5d 100644 --- a/test/e2e/app-dir/segment-cache/no-prefetch/no-prefetch.test.ts +++ b/test/e2e/app-dir/segment-cache/no-prefetch/no-prefetch.test.ts @@ -1,5 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -import { createRouterAct } from '../router-act' +import { createRouterAct } from 'e2e-utils/router-act' describe('navigating without a prefetch', () => { const { next } = nextTestSetup({ diff --git a/test/e2e/app-dir/segment-cache/prefetch-auto/prefetch-auto.test.ts b/test/e2e/app-dir/segment-cache/prefetch-auto/prefetch-auto.test.ts index fb2c6f4fc9882..3585dd3688096 100644 --- a/test/e2e/app-dir/segment-cache/prefetch-auto/prefetch-auto.test.ts +++ b/test/e2e/app-dir/segment-cache/prefetch-auto/prefetch-auto.test.ts @@ -1,6 +1,6 @@ import { nextTestSetup } from 'e2e-utils' +import { createRouterAct } from 'e2e-utils/router-act' import type * as Playwright from 'playwright' -import { createRouterAct } from '../router-act' describe('', () => { const { next, isNextDev } = nextTestSetup({ diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/prefetch-layout-sharing.test.ts b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/prefetch-layout-sharing.test.ts index f1caa014052b3..871eddb5a1967 100644 --- a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/prefetch-layout-sharing.test.ts +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/prefetch-layout-sharing.test.ts @@ -1,7 +1,7 @@ import { nextTestSetup } from 'e2e-utils' -import { Playwright as NextBrowser } from '../../../../lib/next-webdriver' +import { createRouterAct } from 'e2e-utils/router-act' +import { Playwright as NextBrowser } from 'next-webdriver' import type * as Playwright from 'playwright' -import { createRouterAct } from '../router-act' describe('layout sharing in non-static prefetches', () => { const { next, isNextDev } = nextTestSetup({ diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/prefetch-runtime.test.ts b/test/e2e/app-dir/segment-cache/prefetch-runtime/prefetch-runtime.test.ts index 873a8457daf96..9c2167690b63a 100644 --- a/test/e2e/app-dir/segment-cache/prefetch-runtime/prefetch-runtime.test.ts +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/prefetch-runtime.test.ts @@ -1,6 +1,6 @@ import { nextTestSetup } from 'e2e-utils' +import { createRouterAct } from 'e2e-utils/router-act' import type * as Playwright from 'playwright' -import { createRouterAct } from '../router-act' describe('runtime prefetching', () => { const { next, isNextDev, isNextDeploy } = nextTestSetup({ diff --git a/test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts b/test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts index 5783ef8f6db10..78edc9bb801e9 100644 --- a/test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts +++ b/test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts @@ -1,6 +1,6 @@ import { nextTestSetup } from 'e2e-utils' +import { createRouterAct } from 'e2e-utils/router-act' import type * as Playwright from 'playwright' -import { createRouterAct } from '../router-act' describe('segment cache prefetch scheduling', () => { const { next, isNextDev } = nextTestSetup({ diff --git a/test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts b/test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts index 0ba07df51c9ac..f02cd907e3150 100644 --- a/test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts +++ b/test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts @@ -1,5 +1,5 @@ import { isNextDev, isNextDeploy, createNext } from 'e2e-utils' -import { createRouterAct } from '../router-act' +import { createRouterAct } from 'e2e-utils/router-act' import { createTestDataServer } from 'test-data-service/writer' import { createTestLog } from 'test-log' import { findPort } from 'next-test-utils' diff --git a/test/e2e/app-dir/segment-cache/search-params/segment-cache-search-params-shared-loading-state.test.ts b/test/e2e/app-dir/segment-cache/search-params/segment-cache-search-params-shared-loading-state.test.ts index 7dc3755af4ee2..3049ef9ea44f9 100644 --- a/test/e2e/app-dir/segment-cache/search-params/segment-cache-search-params-shared-loading-state.test.ts +++ b/test/e2e/app-dir/segment-cache/search-params/segment-cache-search-params-shared-loading-state.test.ts @@ -1,5 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -import { createRouterAct } from '../router-act' +import { createRouterAct } from 'e2e-utils/router-act' describe('segment cache (search params shared loading state)', () => { const { next, isNextDev } = nextTestSetup({ diff --git a/test/e2e/app-dir/segment-cache/search-params/segment-cache-search-params.test.ts b/test/e2e/app-dir/segment-cache/search-params/segment-cache-search-params.test.ts index a86678fce7e72..f15e5d26b486b 100644 --- a/test/e2e/app-dir/segment-cache/search-params/segment-cache-search-params.test.ts +++ b/test/e2e/app-dir/segment-cache/search-params/segment-cache-search-params.test.ts @@ -1,6 +1,6 @@ import { nextTestSetup } from 'e2e-utils' -import { createRouterAct } from '../router-act' -import { retry } from '../../../../lib/next-test-utils' +import { createRouterAct } from 'e2e-utils/router-act' +import { retry } from 'next-test-utils' describe('segment cache (search params)', () => { const { next, isNextDev, isNextDeploy } = nextTestSetup({ diff --git a/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts b/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts index 18a68171826b0..67f637cf2024f 100644 --- a/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts +++ b/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts @@ -1,6 +1,6 @@ import { nextTestSetup } from 'e2e-utils' +import { createRouterAct } from 'e2e-utils/router-act' import type * as Playwright from 'playwright' -import { createRouterAct } from '../router-act' describe('segment cache (staleness)', () => { const { next, isNextDev, isNextDeploy } = nextTestSetup({ diff --git a/test/e2e/app-dir/segment-cache/router-act.ts b/test/lib/e2e-utils/router-act.ts similarity index 99% rename from test/e2e/app-dir/segment-cache/router-act.ts rename to test/lib/e2e-utils/router-act.ts index 6d2c97be53ab0..3722f8fb4d1f4 100644 --- a/test/e2e/app-dir/segment-cache/router-act.ts +++ b/test/lib/e2e-utils/router-act.ts @@ -19,8 +19,6 @@ type PendingRSCRequest = { didProcess: boolean } -let currentBatch: Batch | null = null - type ExpectedResponseConfig = { includes: string block?: boolean | 'reject' @@ -59,6 +57,8 @@ type ActConfig = export function createRouterAct( page: Playwright.Page ): (scope: () => Promise | T, config?: ActConfig) => Promise { + let currentBatch: Batch | null = null + /** * Test utility for requests initiated by the Next.js Router, such as * prefetches and navigations. Calls the given async function then intercepts @@ -212,7 +212,7 @@ export function createRouterAct( batch.pendingRequests = new Set() batch.pendingRequestChecks = new Set() await Promise.all( - Array.from(orphanedRequests).map((item) => item.route.continue()) + Array.from(orphanedRequests).map((item) => item.route?.continue()) ) didHardNavigate = true } @@ -241,7 +241,7 @@ export function createRouterAct( const timerId = setTimeout(() => { error.message = 'Timed out waiting for a request to be initiated.' reject(error) - }, 500) + }, 1000) onDidIssueFirstRequest = () => { clearTimeout(timerId) resolve()