From deab0fecc3ca77861e90cdead82621ca42796e5f Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Fri, 18 Sep 2026 09:18:03 +0200 Subject: [PATCH] test: unflake instant-validation (#98854) - in `dev`, wait for validation to run before asserting on a redbox being open (encapsulated in `getInstantInsight`), which should reduce flakiness in webpack - in `start`, check if the build actually succeeded before running tests. these builds seem to occasionally fail, but the cause is currently unknown --- .../next/src/server/app-render/app-render.tsx | 38 ++++++++-- .../instant-validation-build.test.ts | 9 ++- .../instant-validation-causes.test.ts | 72 ++----------------- .../instant-validation-level-default.test.ts | 30 +++++++- .../instant-validation-level-error.test.ts | 34 +++++++-- ...tant-validation-level-manual-error.test.ts | 30 ++++++-- ...nt-validation-level-manual-warning.test.ts | 38 +++++++--- .../instant-validation-level-warning.test.ts | 34 +++++++-- .../instant-validation-static-shells.test.ts | 23 +++++- .../app-dir/instant-validation/client.util.ts | 7 +- .../instant-validation/harness.util.ts | 21 +++++- .../head-and-reporting.util.ts | 43 +++++------ .../instant-validation/parallel-slots.test.ts | 24 ++++--- .../instant-validation/server-errors.test.ts | 13 +++- .../slots-and-groups.util.ts | 40 ++++++----- .../suspense-boundaries.util.ts | 35 ++++----- .../sync-io-and-blocking.util.ts | 24 ++++--- ...use-cache-og-image-top-level-await.test.ts | 9 ++- test/lib/e2e-utils/instant-validation.ts | 37 ++++++++-- 19 files changed, 376 insertions(+), 185 deletions(-) diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 8a16c769e7b2..dbfd429e57d3 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -5280,7 +5280,13 @@ async function resolveLazyDevValidationInputs( } if ('syncInterruptReason' in inputs) { - await logMessagesAndSendErrorsToBrowser([inputs.syncInterruptReason], ctx) + await logMessagesAndSendErrorsToBrowser( + [inputs.syncInterruptReason], + ctx, + // We're not going to run validation, so mark this as the validation result for tests. + { logAsValidationResult: true } + ) + return VALIDATION_BAILOUT } return inputs @@ -5291,7 +5297,12 @@ function forwardErrorsFromWarmRender( ctx: AppRenderContext ) { if ('syncInterruptReason' in inputs) { - void logMessagesAndSendErrorsToBrowser([inputs.syncInterruptReason], ctx) + void logMessagesAndSendErrorsToBrowser( + [inputs.syncInterruptReason], + ctx, + // We're not going to run validation, so mark this as the validation result for tests. + { logAsValidationResult: true } + ) return true } @@ -6397,11 +6408,24 @@ function createAsyncApiPromises( */ async function logMessagesAndSendErrorsToBrowser( messages: unknown[], - ctx: AppRenderContext + ctx: AppRenderContext, + options: { logAsValidationResult?: boolean } = {} ): Promise { - const { htmlRequestId, renderOpts } = ctx + const logAsValidationResult = + process.env.__NEXT_TEST_MODE && + process.env.NEXT_TEST_LOG_VALIDATION && + options.logAsValidationResult + + const { htmlRequestId, requestId, renderOpts } = ctx + const url = ctx.url.href const { sendErrorsToBrowser } = renderOpts + if (logAsValidationResult) { + console.log( + formatValidationEvent({ type: 'validation_start', requestId, url }) + ) + } + const errors: Error[] = [] for (const message of messages) { // Log the error to the CLI. Prevent the logs from being dimmed, which we @@ -6421,6 +6445,12 @@ async function logMessagesAndSendErrorsToBrowser( } } + if (logAsValidationResult) { + console.log( + formatValidationEvent({ type: 'validation_end', requestId, url }) + ) + } + if (errors.length > 0) { if (!sendErrorsToBrowser) { throw new InvariantError( diff --git a/test/e2e/app-dir/instant-validation-build/instant-validation-build.test.ts b/test/e2e/app-dir/instant-validation-build/instant-validation-build.test.ts index 997056e17e61..e320ca68198a 100644 --- a/test/e2e/app-dir/instant-validation-build/instant-validation-build.test.ts +++ b/test/e2e/app-dir/instant-validation-build/instant-validation-build.test.ts @@ -42,7 +42,14 @@ describe('instant-validation-build', () => { } beforeAll(async () => { - await next.build({ args: ['--experimental-build-mode', 'compile'] }) + const result = await next.build({ + args: ['--experimental-build-mode', 'compile'], + }) + if (result.exitCode !== 0) { + throw new Error( + `Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}` + ) + } }) describe('basic dynamic hole detection', () => { diff --git a/test/e2e/app-dir/instant-validation-causes/instant-validation-causes.test.ts b/test/e2e/app-dir/instant-validation-causes/instant-validation-causes.test.ts index 059536718bd0..1b69a0f6b6f3 100644 --- a/test/e2e/app-dir/instant-validation-causes/instant-validation-causes.test.ts +++ b/test/e2e/app-dir/instant-validation-causes/instant-validation-causes.test.ts @@ -1,6 +1,5 @@ import { nextTestSetup } from 'e2e-utils' -import { retry } from 'next-test-utils' -import type { ValidationEvent } from 'next/dist/server/app-render/dev-validation-events' +import { createGetInstantInsight } from 'e2e-utils/instant-validation' describe('instant validation causes', () => { const { next, skipped, isNextDev } = nextTestSetup({ @@ -28,67 +27,11 @@ describe('instant validation causes', () => { return next.cliOutput.slice(currentCliOutputIndex) } - function parseValidationMessages(output: string): ValidationEvent[] { - const messageRe = /(.*?)<\/VALIDATION_MESSAGE>/g - const events: ValidationEvent[] = [] - let match: RegExpExecArray | null - while ((match = messageRe.exec(output)) !== null) { - try { - events.push(JSON.parse(match[1])) - } catch (err) { - throw new Error(`Failed to parse message '${match[1]}'`, { - cause: err, - }) - } - } - return events - } - - function normalizeValidationUrl(url: string): string { - const parsed = new URL(url, 'http://n') - parsed.searchParams.delete('_rsc') - return parsed.pathname + parsed.search + parsed.hash - } - - async function waitForValidation(targetUrl: string) { - const parsedTargetUrl = new URL(targetUrl) - const relativeTargetUrl = - parsedTargetUrl.pathname + parsedTargetUrl.search + parsedTargetUrl.hash - - const requestId = await retry( - async () => { - const events = parseValidationMessages(getCliOutputSinceMark()) - const start = events.find( - (e) => - e.type === 'validation_start' && - normalizeValidationUrl(e.url) === relativeTargetUrl - ) - expect(start).toBeDefined() - return start!.requestId - }, - undefined, - undefined, - `wait for validation of '${relativeTargetUrl}' to start` - ) - - await retry( - async () => { - const events = parseValidationMessages(getCliOutputSinceMark()) - const end = events.find( - (e) => e.type === 'validation_end' && e.requestId === requestId - ) - expect(end).toBeDefined() - }, - undefined, - undefined, - 'wait for validation to end' - ) - } + const getInstantInsight = createGetInstantInsight(getCliOutputSinceMark, next) it('named export - export { instant }', async () => { const browser = await next.browser('/named-export') - await waitForValidation(await browser.url()) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -117,8 +60,7 @@ describe('instant validation causes', () => { it('aliased export - export { instantConfig as instant }', async () => { const browser = await next.browser('/aliased-export') - await waitForValidation(await browser.url()) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -147,8 +89,7 @@ describe('instant validation causes', () => { it('re-export - export { instant } from "./config"', async () => { const browser = await next.browser('/reexport') - await waitForValidation(await browser.url()) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -177,11 +118,10 @@ describe('instant validation causes', () => { it('indirect export - const instantConfig = _instant; export { instantConfig as instant }', async () => { const browser = await next.browser('/indirect-export') - await waitForValidation(await browser.url()) // Ideally we'd be pointing at the original value declaration. // We're not following declarations recursively mostly to keep the implementation simpler // presuming that almost all configs are just `export const instant = ...` - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { diff --git a/test/e2e/app-dir/instant-validation-level-default/instant-validation-level-default.test.ts b/test/e2e/app-dir/instant-validation-level-default/instant-validation-level-default.test.ts index 1fe38d480959..472b8d1dcf64 100644 --- a/test/e2e/app-dir/instant-validation-level-default/instant-validation-level-default.test.ts +++ b/test/e2e/app-dir/instant-validation-level-default/instant-validation-level-default.test.ts @@ -1,5 +1,8 @@ import { nextTestSetup } from 'e2e-utils' -import { expectBuildValidationSkipped } from 'e2e-utils/instant-validation' +import { + createGetInstantInsight, + expectBuildValidationSkipped, +} from 'e2e-utils/instant-validation' import { waitForNoErrorToast } from '../../../lib/next-test-utils' // This fixture intentionally omits `experimental.instantInsights` from @@ -27,9 +30,30 @@ describe('instant validation - default level', () => { return } + let currentCliOutputIndex = 0 + beforeEach(() => { + currentCliOutputIndex = next.cliOutput.length + }) + + function getCliOutputSinceMark(): string { + if (next.cliOutput.length < currentCliOutputIndex) { + currentCliOutputIndex = 0 + } + return next.cliOutput.slice(currentCliOutputIndex) + } + + const getInstantInsight = createGetInstantInsight(getCliOutputSinceMark, next) + if (isNextStart) { beforeAll(async () => { - await next.build({ args: ['--experimental-build-mode', 'compile'] }) + const result = await next.build({ + args: ['--experimental-build-mode', 'compile'], + }) + if (result.exitCode !== 0) { + throw new Error( + `Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}` + ) + } }) afterEach(async () => { await next.stop() @@ -55,7 +79,7 @@ describe('instant validation - default level', () => { describe('dev', () => { it('bare page: framework default matches `warning`, implicit validation fires', async () => { const browser = await next.browser('/bare') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered uncached data during a navigation.", "environmentLabel": "Server", diff --git a/test/e2e/app-dir/instant-validation-level-error/instant-validation-level-error.test.ts b/test/e2e/app-dir/instant-validation-level-error/instant-validation-level-error.test.ts index 6a2b1f53a682..ff916782cc37 100644 --- a/test/e2e/app-dir/instant-validation-level-error/instant-validation-level-error.test.ts +++ b/test/e2e/app-dir/instant-validation-level-error/instant-validation-level-error.test.ts @@ -1,5 +1,6 @@ import { nextTestSetup } from 'e2e-utils' import { + createGetInstantInsight, expectBuildValidationSkipped, extractBuildValidationError, } from 'e2e-utils/instant-validation' @@ -21,9 +22,30 @@ describe('instant validation - level error', () => { return } + let currentCliOutputIndex = 0 + beforeEach(() => { + currentCliOutputIndex = next.cliOutput.length + }) + + function getCliOutputSinceMark(): string { + if (next.cliOutput.length < currentCliOutputIndex) { + currentCliOutputIndex = 0 + } + return next.cliOutput.slice(currentCliOutputIndex) + } + + const getInstantInsight = createGetInstantInsight(getCliOutputSinceMark, next) + if (isNextStart) { beforeAll(async () => { - await next.build({ args: ['--experimental-build-mode', 'compile'] }) + const result = await next.build({ + args: ['--experimental-build-mode', 'compile'], + }) + if (result.exitCode !== 0) { + throw new Error( + `Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}` + ) + } }) afterEach(async () => { await next.stop() @@ -58,7 +80,7 @@ describe('instant validation - level error', () => { describe('dev', () => { it('bare page: implicit validation surfaces a redbox (error level fires)', async () => { const browser = await next.browser('/bare') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered uncached data during a navigation.", "environmentLabel": "Server", @@ -75,7 +97,7 @@ describe('instant validation - level error', () => { it('explicit-error page: explicit override at the configured level, instant redbox in dev', async () => { const browser = await next.browser('/explicit-error') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -104,7 +126,7 @@ describe('instant validation - level error', () => { it('explicit-true page: aliases to error level, instant redbox in dev', async () => { const browser = await next.browser('/explicit-true') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -133,7 +155,7 @@ describe('instant validation - level error', () => { it('explicit-warning page: per-segment de-escalation still validates in dev', async () => { const browser = await next.browser('/explicit-warning') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -171,7 +193,7 @@ describe('instant validation - level error', () => { // that's per-segment — it doesn't shield descendants. The bare // page should still surface an instant redbox in dev. const browser = await next.browser('/layered') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered uncached data during a navigation.", "environmentLabel": "Server", diff --git a/test/e2e/app-dir/instant-validation-level-manual-error/instant-validation-level-manual-error.test.ts b/test/e2e/app-dir/instant-validation-level-manual-error/instant-validation-level-manual-error.test.ts index f8a149c2551c..ba4858713d95 100644 --- a/test/e2e/app-dir/instant-validation-level-manual-error/instant-validation-level-manual-error.test.ts +++ b/test/e2e/app-dir/instant-validation-level-manual-error/instant-validation-level-manual-error.test.ts @@ -1,5 +1,6 @@ import { nextTestSetup } from 'e2e-utils' import { + createGetInstantInsight, expectBuildValidationSkipped, extractBuildValidationError, } from 'e2e-utils/instant-validation' @@ -21,9 +22,30 @@ describe('instant validation - level manual-error', () => { return } + let currentCliOutputIndex = 0 + beforeEach(() => { + currentCliOutputIndex = next.cliOutput.length + }) + + function getCliOutputSinceMark(): string { + if (next.cliOutput.length < currentCliOutputIndex) { + currentCliOutputIndex = 0 + } + return next.cliOutput.slice(currentCliOutputIndex) + } + + const getInstantInsight = createGetInstantInsight(getCliOutputSinceMark, next) + if (isNextStart) { beforeAll(async () => { - await next.build({ args: ['--experimental-build-mode', 'compile'] }) + const result = await next.build({ + args: ['--experimental-build-mode', 'compile'], + }) + if (result.exitCode !== 0) { + throw new Error( + `Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}` + ) + } }) afterEach(async () => { await next.stop() @@ -65,7 +87,7 @@ describe('instant validation - level manual-error', () => { it('explicit-error page: explicit override at the configured level, instant redbox in dev', async () => { const browser = await next.browser('/explicit-error') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -94,7 +116,7 @@ describe('instant validation - level manual-error', () => { it('explicit-true page: aliases to error level, instant redbox in dev', async () => { const browser = await next.browser('/explicit-true') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -123,7 +145,7 @@ describe('instant validation - level manual-error', () => { it('explicit-warning page: per-segment de-escalation still validates in dev', async () => { const browser = await next.browser('/explicit-warning') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { diff --git a/test/e2e/app-dir/instant-validation-level-manual-warning/instant-validation-level-manual-warning.test.ts b/test/e2e/app-dir/instant-validation-level-manual-warning/instant-validation-level-manual-warning.test.ts index 8fe66082a719..b32c7e84bc50 100644 --- a/test/e2e/app-dir/instant-validation-level-manual-warning/instant-validation-level-manual-warning.test.ts +++ b/test/e2e/app-dir/instant-validation-level-manual-warning/instant-validation-level-manual-warning.test.ts @@ -1,5 +1,6 @@ import { nextTestSetup } from 'e2e-utils' import { + createGetInstantInsight, expectBuildValidationSkipped, extractBuildValidationError, parseValidationMessages, @@ -23,9 +24,30 @@ describe('instant validation - level manual-warning', () => { return } + let currentCliOutputIndex = 0 + beforeEach(() => { + currentCliOutputIndex = next.cliOutput.length + }) + + function getCliOutputSinceMark(): string { + if (next.cliOutput.length < currentCliOutputIndex) { + currentCliOutputIndex = 0 + } + return next.cliOutput.slice(currentCliOutputIndex) + } + + const getInstantInsight = createGetInstantInsight(getCliOutputSinceMark, next) + if (isNextStart) { beforeAll(async () => { - await next.build({ args: ['--experimental-build-mode', 'compile'] }) + const result = await next.build({ + args: ['--experimental-build-mode', 'compile'], + }) + if (result.exitCode !== 0) { + throw new Error( + `Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}` + ) + } }) afterEach(async () => { await next.stop() @@ -79,7 +101,7 @@ describe('instant validation - level manual-warning', () => { const browser = await next.browser( '/with-root-suspense/explicit-error' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -110,7 +132,7 @@ describe('instant validation - level manual-warning', () => { const browser = await next.browser( '/with-root-suspense/explicit-true' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -141,7 +163,7 @@ describe('instant validation - level manual-warning', () => { const browser = await next.browser( '/with-root-suspense/explicit-warning' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -224,7 +246,7 @@ describe('instant validation - level manual-warning', () => { // the top of the page. The captured snapshot should NOT contain // the "Instant" label — that's the proof that instant validation // did not run under 'manual-warning'. - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered uncached data during prerendering.", "environmentLabel": "Server", @@ -243,7 +265,7 @@ describe('instant validation - level manual-warning', () => { const browser = await next.browser( '/without-root-suspense/explicit-error' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered uncached data during prerendering.", "environmentLabel": "Server", @@ -262,7 +284,7 @@ describe('instant validation - level manual-warning', () => { const browser = await next.browser( '/without-root-suspense/explicit-true' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered uncached data during prerendering.", "environmentLabel": "Server", @@ -281,7 +303,7 @@ describe('instant validation - level manual-warning', () => { const browser = await next.browser( '/without-root-suspense/explicit-warning' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered uncached data during prerendering.", "environmentLabel": "Server", diff --git a/test/e2e/app-dir/instant-validation-level-warning/instant-validation-level-warning.test.ts b/test/e2e/app-dir/instant-validation-level-warning/instant-validation-level-warning.test.ts index b3efbb269b7c..d17120970f63 100644 --- a/test/e2e/app-dir/instant-validation-level-warning/instant-validation-level-warning.test.ts +++ b/test/e2e/app-dir/instant-validation-level-warning/instant-validation-level-warning.test.ts @@ -1,5 +1,6 @@ import { nextTestSetup } from 'e2e-utils' import { + createGetInstantInsight, expectBuildValidationSkipped, extractBuildValidationError, } from 'e2e-utils/instant-validation' @@ -21,9 +22,30 @@ describe('instant validation - level warning', () => { return } + let currentCliOutputIndex = 0 + beforeEach(() => { + currentCliOutputIndex = next.cliOutput.length + }) + + function getCliOutputSinceMark(): string { + if (next.cliOutput.length < currentCliOutputIndex) { + currentCliOutputIndex = 0 + } + return next.cliOutput.slice(currentCliOutputIndex) + } + + const getInstantInsight = createGetInstantInsight(getCliOutputSinceMark, next) + if (isNextStart) { beforeAll(async () => { - await next.build({ args: ['--experimental-build-mode', 'compile'] }) + const result = await next.build({ + args: ['--experimental-build-mode', 'compile'], + }) + if (result.exitCode !== 0) { + throw new Error( + `Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}` + ) + } }) afterEach(async () => { await next.stop() @@ -58,7 +80,7 @@ describe('instant validation - level warning', () => { describe('dev', () => { it('bare page: implicit validation surfaces a redbox (warning level fires)', async () => { const browser = await next.browser('/bare') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered uncached data during a navigation.", "environmentLabel": "Server", @@ -75,7 +97,7 @@ describe('instant validation - level warning', () => { it('explicit-error page: instant redbox surfaces in dev', async () => { const browser = await next.browser('/explicit-error') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -104,7 +126,7 @@ describe('instant validation - level warning', () => { it('explicit-true page: aliases to warning level, instant redbox in dev', async () => { const browser = await next.browser('/explicit-true') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -133,7 +155,7 @@ describe('instant validation - level warning', () => { it('explicit-warning page: explicit override at the configured level, instant redbox in dev', async () => { const browser = await next.browser('/explicit-warning') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -171,7 +193,7 @@ describe('instant validation - level warning', () => { // that's per-segment — it doesn't shield descendants. The bare // page should still surface an instant redbox in dev. const browser = await next.browser('/layered') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered uncached data during a navigation.", "environmentLabel": "Server", diff --git a/test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts b/test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts index 9ad2b982ec10..3a6c23f27adc 100644 --- a/test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts +++ b/test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts @@ -1,4 +1,5 @@ import { nextTestSetup } from 'e2e-utils' +import { createGetInstantInsight } from 'e2e-utils/instant-validation' import { waitForNoErrorToast } from 'next-test-utils' import { join } from 'node:path' @@ -35,14 +36,34 @@ describe('instant validation', () => { const { next, isNextDev } = nextTestSetup({ files: join(__dirname, 'fixtures', 'invalid-blocking-page-below-static'), skipStart: true, + env: { + NEXT_TEST_LOG_VALIDATION: '1', + }, }) + let currentCliOutputIndex = 0 + beforeEach(() => { + currentCliOutputIndex = next.cliOutput.length + }) + + function getCliOutputSinceMark(): string { + if (next.cliOutput.length < currentCliOutputIndex) { + currentCliOutputIndex = 0 + } + return next.cliOutput.slice(currentCliOutputIndex) + } + + const getInstantInsight = createGetInstantInsight( + getCliOutputSinceMark, + next + ) + if (isNextDev) { beforeAll(() => next.start()) it('errors in dev', async () => { const browser = await next.browser('/blocking-page-below-static') await browser.elementByCss('main') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered uncached data during prerendering.", "environmentLabel": "Server", diff --git a/test/e2e/app-dir/instant-validation/client.util.ts b/test/e2e/app-dir/instant-validation/client.util.ts index 99b8281c9595..db645261899c 100644 --- a/test/e2e/app-dir/instant-validation/client.util.ts +++ b/test/e2e/app-dir/instant-validation/client.util.ts @@ -25,6 +25,7 @@ export function registerClientTests(ctx: InstantValidationCaseContext) { isClientNav, navigateTo, expectNoDevValidationErrors, + getInstantInsight, getCliOutputSinceMark, prerender, } = ctx @@ -35,7 +36,7 @@ export function registerClientTests(ctx: InstantValidationCaseContext) { const browser = await navigateTo( '/suspense-in-root/static/invalid-client-data-blocks-validation' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -208,6 +209,7 @@ export function registerClientTests(ctx: InstantValidationCaseContext) { const browser = await navigateTo( '/suspense-in-root/static/invalid-client-error-in-parent-blocks-children' ) + await waitForValidation(await browser.url(), getCliOutputSinceMark) // We expect a collapsed redbox. We need to open it to assert on the messages. await openRedbox(browser) @@ -313,6 +315,7 @@ export function registerClientTests(ctx: InstantValidationCaseContext) { const browser = await navigateTo( '/suspense-in-root/static/invalid-error-in-node-modules-blocks-children' ) + await waitForValidation(await browser.url(), getCliOutputSinceMark) // We expect a collapsed redbox. We need to open it to assert on the messages. await openRedbox(browser) @@ -399,7 +402,7 @@ export function registerClientTests(ctx: InstantValidationCaseContext) { const browser = await navigateTo( '/suspense-in-root/static/invalid-csr-bailout-blocks-children' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` [ { "description": "Route "/suspense-in-root/static/invalid-csr-bailout-blocks-children": Could not validate \`instant\` because the target segment was prevented from rendering, likely due to the following error.", diff --git a/test/e2e/app-dir/instant-validation/harness.util.ts b/test/e2e/app-dir/instant-validation/harness.util.ts index 7f8cee97314d..ad33ecedf9ff 100644 --- a/test/e2e/app-dir/instant-validation/harness.util.ts +++ b/test/e2e/app-dir/instant-validation/harness.util.ts @@ -1,5 +1,8 @@ import { nextTestSetup, type NextInstance, type Playwright } from 'e2e-utils' -import { waitForValidation } from 'e2e-utils/instant-validation' +import { + createGetInstantInsight, + waitForValidation, +} from 'e2e-utils/instant-validation' import { retry, waitForNoErrorToast } from '../../../lib/next-test-utils' export interface InstantValidationCaseContext { @@ -20,6 +23,7 @@ export interface InstantValidationCaseContext { browser: Playwright, url: string ) => Promise + getInstantInsight: ReturnType getCliOutputSinceMark: () => string /** Prerender a single page with `--experimental-build-mode generate` */ @@ -74,6 +78,11 @@ export function runInstantValidationTests( return next.cliOutput.slice(currentCliOutputIndex) } + const getInstantInsight = createGetInstantInsight( + getCliOutputSinceMark, + next + ) + async function restartDevServerToEnsureColdCaches() { if (isNextDev) { // Ensure caches are cold. @@ -87,7 +96,14 @@ export function runInstantValidationTests( if (isNextStart) { // Compile the app first so that `prerender` can run individual prerenders. beforeAll(async () => { - await next.build({ args: ['--experimental-build-mode', 'compile'] }) + const result = await next.build({ + args: ['--experimental-build-mode', 'compile'], + }) + if (result.exitCode !== 0) { + throw new Error( + `Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}` + ) + } }) afterEach(async () => { await next.stop() @@ -246,6 +262,7 @@ export function runInstantValidationTests( warmCachesAndNavigateTo, restartDevServerToEnsureColdCaches, expectNoDevValidationErrors, + getInstantInsight, getCliOutputSinceMark, prerender, }) diff --git a/test/e2e/app-dir/instant-validation/head-and-reporting.util.ts b/test/e2e/app-dir/instant-validation/head-and-reporting.util.ts index aa3c6c52353c..a44542982663 100644 --- a/test/e2e/app-dir/instant-validation/head-and-reporting.util.ts +++ b/test/e2e/app-dir/instant-validation/head-and-reporting.util.ts @@ -18,6 +18,7 @@ export function registerHeadAndReportingTests( warmCachesAndNavigateTo, restartDevServerToEnsureColdCaches, expectNoDevValidationErrors, + getInstantInsight, getCliOutputSinceMark, prerender, } = ctx @@ -62,7 +63,7 @@ export function registerHeadAndReportingTests( const browser = await navigateTo( '/suspense-in-root/head/invalid-runtime-viewport-in-static' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -120,7 +121,7 @@ export function registerHeadAndReportingTests( const browser = await navigateTo( '/suspense-in-root/head/invalid-dynamic-viewport-in-runtime' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -216,7 +217,7 @@ export function registerHeadAndReportingTests( '/suspense-in-root/head/invalid-dynamic-viewport-in-blocking-inside-static' ) // TODO(instant-validation): why aren't we pointing to `await connection()` here? - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -287,7 +288,7 @@ export function registerHeadAndReportingTests( const browser = await navigateTo( '/suspense-in-root/static/multi-depth-deferred-fallback/inner' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js could not validate that a segment in your UI has instant navigation.", "environmentLabel": "Server", @@ -344,7 +345,7 @@ export function registerHeadAndReportingTests( const browser = await navigateTo( '/suspense-in-root/static/test-firstmod/inter/inner' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js could not validate that a segment in your UI has instant navigation.", "environmentLabel": "Server", @@ -399,7 +400,7 @@ export function registerHeadAndReportingTests( const browser = await navigateTo( '/suspense-in-root/static/test-multi-unrendered' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js could not validate that a segment in your UI has instant navigation.", "environmentLabel": "Server", @@ -530,7 +531,7 @@ export function registerHeadAndReportingTests( const browser = await navigateTo( '/suspense-in-root/disable-validation/disable-build' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -617,7 +618,7 @@ export function registerHeadAndReportingTests( it('invalid - unguarded params in a runtime-prefetchable shell', async () => { if (isNextDev) { const browser = await navigateTo('/shells/invalid-runtime-params/123') - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -724,7 +725,7 @@ export function registerHeadAndReportingTests( }" `) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -785,7 +786,7 @@ export function registerHeadAndReportingTests( '/shells/invalid-static-with-gsp-metadata/123' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -833,7 +834,7 @@ export function registerHeadAndReportingTests( const browser = await navigateTo( '/shells/invalid-static-with-gsp/123' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -908,7 +909,7 @@ export function registerHeadAndReportingTests( it('with cold caches', async () => { if (isNextDev) { const browser = await navigateTo(routeInBrowser) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -963,7 +964,7 @@ export function registerHeadAndReportingTests( it('with warm caches', async () => { const browser = await warmCachesAndNavigateTo(routeInBrowser) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1052,7 +1053,7 @@ export function registerHeadAndReportingTests( const browser = await navigateTo( '/shells/invalid-navigation-without-suspense' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1125,7 +1126,7 @@ export function registerHeadAndReportingTests( const browser = await navigateTo( '/shells/invalid-prefetch-without-suspense' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1309,7 +1310,7 @@ export function registerHeadAndReportingTests( it('with cold caches', async () => { if (isNextDev) { const browser = await navigateTo(route) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1365,7 +1366,7 @@ export function registerHeadAndReportingTests( it('with warm caches', async () => { const browser = await warmCachesAndNavigateTo(route) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1404,7 +1405,7 @@ export function registerHeadAndReportingTests( const browser = await navigateTo( '/suspense-in-root/blocking-attribution/dynamic-then-dynamic' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1465,7 +1466,7 @@ export function registerHeadAndReportingTests( '/suspense-in-root/blocking-attribution/runtime-then-runtime' ) if (partialPrefetching) { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1492,7 +1493,7 @@ export function registerHeadAndReportingTests( } `) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1580,7 +1581,7 @@ export function registerHeadAndReportingTests( const browser = await navigateTo( '/suspense-in-root/blocking-attribution/session-then-dynamic' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { diff --git a/test/e2e/app-dir/instant-validation/parallel-slots.test.ts b/test/e2e/app-dir/instant-validation/parallel-slots.test.ts index ed9994745206..8e821964d19d 100644 --- a/test/e2e/app-dir/instant-validation/parallel-slots.test.ts +++ b/test/e2e/app-dir/instant-validation/parallel-slots.test.ts @@ -1,5 +1,6 @@ import { nextTestSetup, type Playwright } from 'e2e-utils' import { + createGetInstantInsight, expectBuildValidationSkipped, expectNoBuildValidationErrors, extractBuildValidationError, @@ -26,7 +27,12 @@ describe('instant validation - parallel slot configs', () => { if (isNextStart) { beforeAll(async () => { - await next.build({ args: ['--experimental-build-mode', 'compile'] }) + const result = await next.build({ + args: ['--experimental-build-mode', 'compile'], + }) + if (result.exitCode !== 0) { + throw new Error('Failed to build. CLI Output:\n\n' + result.cliOutput) + } }) afterEach(async () => { await next.stop() @@ -49,6 +55,8 @@ describe('instant validation - parallel slot configs', () => { return next.cliOutput.slice(currentCliOutputIndex) } + const getInstantInsight = createGetInstantInsight(getCliOutputSinceMark, next) + const prerender = async (pathname: string) => { const args = [ '--experimental-build-mode', @@ -110,7 +118,7 @@ describe('instant validation - parallel slot configs', () => { const browser = await navigateTo( '/suspense-in-root/parallel/slot-config-only' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -168,7 +176,7 @@ describe('instant validation - parallel slot configs', () => { const browser = await navigateTo( '/suspense-in-root/parallel/slot-layout-config' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -247,7 +255,7 @@ describe('instant validation - parallel slot configs', () => { const browser = await navigateTo( '/suspense-in-root/parallel/children-config-with-slot' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -306,7 +314,7 @@ describe('instant validation - parallel slot configs', () => { const browser = await navigateTo( '/suspense-in-root/parallel/fork-layout-config-with-slot' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` [ { "cause": [ @@ -470,7 +478,7 @@ describe('instant validation - parallel slot configs', () => { '/suspense-in-root/parallel/conditional-breadcrumbs/show-both/blocked' if (isNextDev) { const browser = await navigateTo(href) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -528,7 +536,7 @@ describe('instant validation - parallel slot configs', () => { '/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/unblocked' if (isNextDev) { const browser = await navigateTo(href) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js could not validate that a segment in your UI has instant navigation.", "environmentLabel": "Server", @@ -560,7 +568,7 @@ describe('instant validation - parallel slot configs', () => { '/suspense-in-root/parallel/conditional-breadcrumbs/show-only-breadcrumbs/blocked' if (isNextDev) { const browser = await navigateTo(href) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { diff --git a/test/e2e/app-dir/instant-validation/server-errors.test.ts b/test/e2e/app-dir/instant-validation/server-errors.test.ts index 9a6f642f4302..aae8a0198142 100644 --- a/test/e2e/app-dir/instant-validation/server-errors.test.ts +++ b/test/e2e/app-dir/instant-validation/server-errors.test.ts @@ -24,7 +24,14 @@ describe('instant validation - server errors', () => { if (isNextStart) { beforeAll(async () => { - await next.build({ args: ['--experimental-build-mode', 'compile'] }) + const result = await next.build({ + args: ['--experimental-build-mode', 'compile'], + }) + if (result.exitCode !== 0) { + throw new Error( + `Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}` + ) + } }) afterEach(async () => { await next.stop() @@ -74,8 +81,8 @@ describe('instant validation - server errors', () => { : await next.browser( '/suspense-in-root/static/server-error-blocks-children' ) - await waitForRedbox(browser) await waitForValidation(await browser.url(), getCliOutputSinceMark) + await waitForRedbox(browser) const errors = await createRedboxSnapshot(browser, next) expect(errors).toMatchInlineSnapshot(` { @@ -134,8 +141,8 @@ describe('instant validation - server errors', () => { : await next.browser( '/suspense-in-root/static/server-error-inside-boundary' ) - await waitForRedbox(browser) await waitForValidation(await browser.url(), getCliOutputSinceMark) + await waitForRedbox(browser) const errors = await createRedboxSnapshot(browser, next) expect(errors).toMatchInlineSnapshot(` { diff --git a/test/e2e/app-dir/instant-validation/slots-and-groups.util.ts b/test/e2e/app-dir/instant-validation/slots-and-groups.util.ts index 01ee4cd345c6..8a51ef32a3e7 100644 --- a/test/e2e/app-dir/instant-validation/slots-and-groups.util.ts +++ b/test/e2e/app-dir/instant-validation/slots-and-groups.util.ts @@ -7,7 +7,13 @@ import { type InstantValidationCaseContext } from './harness.util' const partialPrefetching = !!process.env.__NEXT_PARTIAL_PREFETCHING export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { - const { isNextDev, navigateTo, expectNoDevValidationErrors, prerender } = ctx + const { + isNextDev, + navigateTo, + expectNoDevValidationErrors, + getInstantInsight, + prerender, + } = ctx describe('invalid - missing suspense in parallel slot', () => { // The "caused by" source differs between bundlers due to parallel @@ -20,7 +26,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -90,7 +96,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -160,7 +166,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -231,7 +237,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -300,7 +306,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -370,7 +376,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -440,7 +446,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -510,7 +516,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -587,7 +593,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -671,7 +677,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -749,7 +755,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -829,7 +835,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -905,7 +911,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -960,7 +966,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1032,7 +1038,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1105,7 +1111,7 @@ export function registerSlotsAndGroupsTests(ctx: InstantValidationCaseContext) { // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { diff --git a/test/e2e/app-dir/instant-validation/suspense-boundaries.util.ts b/test/e2e/app-dir/instant-validation/suspense-boundaries.util.ts index db746ceee0d8..458a80e6a1b1 100644 --- a/test/e2e/app-dir/instant-validation/suspense-boundaries.util.ts +++ b/test/e2e/app-dir/instant-validation/suspense-boundaries.util.ts @@ -17,6 +17,7 @@ export function registerSuspenseBoundariesTests( warmCachesAndNavigateTo, restartDevServerToEnsureColdCaches, expectNoDevValidationErrors, + getInstantInsight, getCliOutputSinceMark, prerender, } = ctx @@ -58,7 +59,7 @@ export function registerSuspenseBoundariesTests( // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -122,7 +123,7 @@ export function registerSuspenseBoundariesTests( const browser = await navigateTo( '/suspense-in-root/static/missing-suspense-around-dynamic' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -181,7 +182,7 @@ export function registerSuspenseBoundariesTests( const browser = await navigateTo( '/suspense-in-root/runtime/missing-suspense-around-dynamic' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -247,7 +248,7 @@ export function registerSuspenseBoundariesTests( // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -310,7 +311,7 @@ export function registerSuspenseBoundariesTests( const browser = await navigateTo( '/suspense-in-root/runtime/missing-suspense-around-dynamic-layout' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -373,7 +374,7 @@ export function registerSuspenseBoundariesTests( '/suspense-in-root/static/missing-suspense-around-params/123' ) if (partialPrefetching) { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -400,7 +401,7 @@ export function registerSuspenseBoundariesTests( } `) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -434,7 +435,7 @@ export function registerSuspenseBoundariesTests( const browser = await navigateTo( '/suspense-in-root/runtime/invalid-no-suspense-around-params/123' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -575,7 +576,7 @@ export function registerSuspenseBoundariesTests( } } else { if (partialPrefetching) { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -601,7 +602,7 @@ export function registerSuspenseBoundariesTests( } `) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -724,7 +725,7 @@ export function registerSuspenseBoundariesTests( }" `) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -809,7 +810,7 @@ export function registerSuspenseBoundariesTests( // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -876,7 +877,7 @@ export function registerSuspenseBoundariesTests( const browser = await navigateTo( '/suspense-in-root/runtime/suspense-too-high' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -954,7 +955,7 @@ export function registerSuspenseBoundariesTests( it('with cold caches', async () => { if (isNextDev) { const browser = await navigateTo(route) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1011,7 +1012,7 @@ export function registerSuspenseBoundariesTests( it('with warm caches', async () => { const browser = await warmCachesAndNavigateTo(route) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1066,7 +1067,7 @@ export function registerSuspenseBoundariesTests( const browser = await navigateTo( '/suspense-in-root/static/invalid-loading-above-route-group' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -1129,7 +1130,7 @@ export function registerSuspenseBoundariesTests( const browser = await navigateTo( '/suspense-in-root/static/invalid-dynamic-layout-with-loading' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { diff --git a/test/e2e/app-dir/instant-validation/sync-io-and-blocking.util.ts b/test/e2e/app-dir/instant-validation/sync-io-and-blocking.util.ts index b59dba8e48ab..c63367867b94 100644 --- a/test/e2e/app-dir/instant-validation/sync-io-and-blocking.util.ts +++ b/test/e2e/app-dir/instant-validation/sync-io-and-blocking.util.ts @@ -10,7 +10,13 @@ const partialPrefetching = !!process.env.__NEXT_PARTIAL_PREFETCHING export function registerSyncIoAndBlockingTests( ctx: InstantValidationCaseContext ) { - const { isNextDev, navigateTo, expectNoDevValidationErrors, prerender } = ctx + const { + isNextDev, + navigateTo, + expectNoDevValidationErrors, + getInstantInsight, + prerender, + } = ctx describe('Sync IO', () => { it('sync IO after session data', async () => { @@ -19,7 +25,7 @@ export function registerSyncIoAndBlockingTests( '/suspense-in-root/sync-io/sync-io-after-cookies' ) if (partialPrefetching) { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered the unstable value Date.now() while prerendering.", "environmentLabel": "Server", @@ -79,7 +85,7 @@ export function registerSyncIoAndBlockingTests( const browser = await navigateTo( '/suspense-in-root/sync-io/sync-io-after-navigation' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered the unstable value Date.now() while prerendering.", "environmentLabel": "Server", @@ -137,7 +143,7 @@ export function registerSyncIoAndBlockingTests( const browser = await navigateTo( '/suspense-in-root/sync-io/sync-io-after-prefetch' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered the unstable value Date.now() while prerendering.", "environmentLabel": "Server", @@ -196,7 +202,7 @@ export function registerSyncIoAndBlockingTests( '/suspense-in-root/sync-io/sync-io-after-cache-with-cookie-input' ) if (partialPrefetching) { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered the unstable value Date.now() while prerendering.", "environmentLabel": "Server", @@ -265,7 +271,7 @@ export function registerSyncIoAndBlockingTests( '/suspense-in-root/sync-io/sync-io-after-cookies-in-generate-metadata' ) if (partialPrefetching) { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "description": "Next.js encountered the unstable value Date.now() while prerendering.", "environmentLabel": "Server", @@ -360,7 +366,7 @@ export function registerSyncIoAndBlockingTests( // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -456,7 +462,7 @@ export function registerSyncIoAndBlockingTests( // This page uses a runtime shell, so it can use cookies await expectNoDevValidationErrors(browser, await browser.url()) } else { - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { @@ -521,7 +527,7 @@ export function registerSyncIoAndBlockingTests( const browser = await navigateTo( '/suspense-in-root/runtime/invalid-blocking-inside-runtime' ) - await expect(browser).toDisplayCollapsedRedbox(` + expect(await getInstantInsight(browser)).toMatchInlineSnapshot(` { "cause": [ { diff --git a/test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts b/test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts index 5cd34efa05e7..864ceaec771f 100644 --- a/test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts +++ b/test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts @@ -8,7 +8,14 @@ describe('use-cache-og-image-top-level-await', () => { if (isNextStart) { beforeAll(async () => { - await next.build({ args: ['--experimental-build-mode', 'compile'] }) + const result = await next.build({ + args: ['--experimental-build-mode', 'compile'], + }) + if (result.exitCode !== 0) { + throw new Error( + `Build exited with exit code ${result.exitCode}. CLI Output:\n\n${result.cliOutput}` + ) + } }) it('should prerender a page whose opengraph image uses a top-level await', async () => { diff --git a/test/lib/e2e-utils/instant-validation.ts b/test/lib/e2e-utils/instant-validation.ts index e8af55aa1cc9..f648b9517689 100644 --- a/test/lib/e2e-utils/instant-validation.ts +++ b/test/lib/e2e-utils/instant-validation.ts @@ -1,6 +1,12 @@ -import { retry } from '../next-test-utils' +import { openRedbox, retry } from '../next-test-utils' +import { + createRedboxSnapshot, + type RedboxSnapshot, +} from '../add-redbox-matchers' import { getDeterministicOutput } from '../../e2e/app-dir/cache-components-errors/utils' import { inspect } from 'util' +import type { Playwright } from '../browsers/playwright' +import type { NextInstance } from '../next-modes/base' import type { ValidationEvent, ValidationStartEvent, @@ -114,6 +120,8 @@ export function normalizeValidationUrl(url: string): string { return parsed.pathname + parsed.search + parsed.hash } +const MAX_VALIDATION_WAIT = 5000 + export async function waitForValidationStart( targetUrl: string, getOutput: () => string @@ -130,10 +138,12 @@ export async function waitForValidationStart( e.type === 'validation_start' && normalizeValidationUrl(e.url) === relativeTargetUrl ) - expect(start).toBeDefined() - return start! as ValidationStartEvent + if (start === undefined) { + throw new Error('Could not find "validation_start" marker') + } + return start as ValidationStartEvent }, - undefined, + MAX_VALIDATION_WAIT, undefined, `wait for validation of '${relativeTargetUrl}' to start` ) @@ -153,10 +163,12 @@ export async function waitForValidationEnd( const end = events.find( (e) => e.type === 'validation_end' && e.requestId === start.requestId ) - expect(end).toBeDefined() + if (end === undefined) { + throw new Error('Could not find "validation_end" marker') + } return end as ValidationEndEvent }, - undefined, + MAX_VALIDATION_WAIT, undefined, 'wait for validation to end' ) @@ -179,6 +191,19 @@ export async function waitForValidation(url: string, getOutput: () => string) { return { start, end } } +export function createGetInstantInsight( + getOutput: () => string, + next: NextInstance +): (browser: Playwright) => Promise { + return async function getInstantInsight(browser) { + // Wait for instant validation to actually run before asserting on it. + // This reduces flakiness in case a redbox is slow to open in CI. + await waitForValidation(await browser.url(), getOutput) + await openRedbox(browser) + return createRedboxSnapshot(browser, next) + } +} + type PrerenderResult = { cliOutput: string exitCode: number | NodeJS.Signals