mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
codex/fallback-root-cache
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2ad16804ac |
test: enable verified assets deploy tests (#98527)
## Summary
Enable the same 11 previously selected deployment-test scopes across 11
assets test files, now in a stack rooted on canary. Remove 11
`skipDeployment` options and their obsolete skip guards. Other mode,
bundler, middleware, and Cache Components exclusions remain in place.
This preserves the selection with passing evidence from the previous
deployment runs. No additional candidate scopes are enabled; excluded
variants are not counted as deployment coverage.
## Verification
- All selected test registration names and assertion bodies match the
previous enabled revision, checked by AST comparison.
- Verified that the canary diff contains only the inventoried exclusions
and their obsolete skip plumbing; other exclusions are preserved.
- Formatting and lint passed; 77 gate infrastructure unit tests passed.
- Full local bootstrap was blocked by missing package-level dependencies
in the temporary worktree. Fresh deployment execution on these rewritten
commits remains to be verified in CI.
<details>
<summary>Preserved scope inventory (11)</summary>
- ID 286: `test/e2e/app-dir/app-css-pageextensions/index.test.ts` —
`describe('app dir - css with pageextensions', () => {
const { next } = nextTestSetup({
files: __dirname,
dependencies: {
'@picocss/pico': '1.5.7',
sass: 'latest',
},
})
describe('css support with pageextensions', () => {
describe('page in app directory with pageextention, css should work', ()
=> {
it('should support global css inside layout', async () => {
const browser = await next.browser('/css-pageextensions')
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('h1')).color`
)
).toBe('rgb(255, 0, 0)')
})
})
})
})`
- ID 292: `test/e2e/app-dir/dynamic-css/index.test.ts` — `describe('app
dir - dynamic css', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should preload all chunks of dynamic component during SSR', async ()
=> {
const $ = await next.render$('/ssr')
const cssLinks = $('link[rel="stylesheet"][data-precedence="dynamic"]')
expect(cssLinks.attr('href')).toContain('.css')
const preloadJsChunks = $('link[rel="preload"]')
expect(preloadJsChunks.attr('as')).toBe('script')
expect(preloadJsChunks.attr('fetchpriority')).toContain(`low`)
})
it('should only apply corresponding css for page loaded that /ssr',
async () => {
const browser = await next.browser('/ssr')
await retry(async () => {
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).color`
)
).toBe('rgb(255, 0, 0)')
// Default border width, which is not effected by bar.css that is not
loaded in /ssr
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).borderWidth`
)
).toBe('0px')
})
})
it('should only apply corresponding css for page loaded in edge
runtime', async () => {
const browser = await next.browser('/ssr/edge')
await retry(async () => {
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).color`
)
).toBe('rgb(255, 0, 0)')
// Default border width, which is not effected by bar.css that is not
loaded in /ssr
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).borderWidth`
)
).toBe('0px')
})
})
it('should only apply corresponding css for page loaded that /another',
async () => {
const browser = await next.browser('/another')
await retry(async () => {
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).color`
)
).not.toBe('rgb(255, 0, 0)')
// Default border width, which is not effected by bar.css that is not
loaded in /ssr
expect(
await browser.eval(
`window.getComputedStyle(document.querySelector('.text')).borderWidth`
)
).toBe('1px')
})
})
it('should not throw with accessing to ALS in preload css', async () =>
{
const output = next.cliOutput
expect(output).not.toContain('was called outside a request scope')
})
})`
- ID 293: `test/e2e/app-dir/emotion-js/index.test.ts` — `describe('app
dir - emotion-js', () => {
const { next } = nextTestSetup({
files: __dirname,
dependencies: {
'@emotion/react': 'latest',
'@emotion/cache': 'latest',
},
})
it('should render emotion-js css with compiler.emotion option
correctly', async () => {
const browser = await next.browser('/')
const el = browser.elementByCss('h1')
expect(await el.text()).toBe('Blue')
await check(
async () =>
await browser.eval(
`window.getComputedStyle(document.querySelector('h1')).color`
),
'rgb(0, 0, 255)'
)
const el2 = browser.elementByCss('p')
expect(await el2.text()).toBe('Red')
await check(
async () =>
await browser.eval(
`window.getComputedStyle(document.querySelector('p')).color`
),
'rgb(255, 0, 0)'
)
})
})`
- ID 294:
`test/e2e/app-dir/global-error/with-style-import/index.test.ts` —
`describe('app dir - global error - with style import', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
it('should render global error with correct styles', async () => {
const browser = await next.browser('/')
if (isNextDev) {
await testDev(browser, /Root Layout Error/)
return
}
const h2 = await browser.elementByCss('h2')
expect(await h2.getComputedCss('color')).toBe('rgb(255, 255, 0)') //
yellow
})
})`
- ID 301: `test/e2e/app-dir/not-found/css-precedence/index.test.ts` —
`describe('not-found app dir css', () => {
const { next } = nextTestSetup({
files: __dirname,
dependencies: {
sass: 'latest',
},
})
it('should load css while navigation between not-found and page', async
() => {
const browser = await next.browser('/')
await check(
async () =>
await browser.eval(
`window.getComputedStyle(document.querySelector('#go-to-404')).backgroundColor`
),
'rgb(0, 128, 0)'
)
await browser.elementByCss('#go-to-404').click()
await browser.waitForElementByCss('#go-to-index')
await check(
async () =>
await browser.eval(
`window.getComputedStyle(document.querySelector('#go-to-index')).backgroundColor`
),
'rgb(0, 128, 0)'
)
await browser.elementByCss('#go-to-index').click()
await browser.waitForElementByCss('#go-to-404')
await check(
async () =>
await browser.eval(
`window.getComputedStyle(document.querySelector('#go-to-404')).backgroundColor`
),
'rgb(0, 128, 0)'
)
})
})`
- ID 308:
`test/e2e/app-dir/turbopack-postcss-multiple-configs/turbopack-postcss-multiple-configs.test.ts`
— `describe('turbopack-postcss-multiple-configs', () => {
const { next, isTurbopack } = nextTestSetup({
files: __dirname,
// Per-directory PostCSS config resolution is a Turbopack-only feature
// (turbopackLocalPostcssConfig). Webpack does not support this feature
and
// does not accept function-valued PostCSS plugins, so skip
non-Turbopack runs.
skipStart: true,
})
if (!isTurbopack) {
it('should only run with Turbopack', () => {})
return
}
beforeAll(async () => {
await next.start()
})
// Each directory's postcss.config.js passes a unique color option to
the
// shared plugin, which replaces `color: red` with the given color.
// In production mode the CSS minifier may shorten named colors to hex
// (e.g. blue → #00f), so we match on patterns that cover both forms.
const DIR_COLORS: Record<number, string | RegExp> = {
1: /blue|#00f/,
2: /purple|#800080/,
3: /orange|#ffa500/,
4: /cyan|#0ff/,
5: /magenta|#f0f/,
}
const DIRS = 5
const FILES_PER_DIR = 3
it('should render all elements with CSS module classes applied', async
() => {
const $ = await next.render$('/')
for (let dir = 1; dir <= DIRS; dir++) {
for (let file = 1; file <= FILES_PER_DIR; file++) {
const padded = String(file).padStart(2, '0')
const id = `dir${dir}-file${padded}`
const el = $(`#${id}`)
expect(el.length).toBe(1)
expect(el.text().trim()).toBe(`dir${dir} file${padded}`)
expect(el.attr('class')).toBeTruthy()
}
}
})
it('should apply per-directory PostCSS transforms with distinct colors',
async () => {
const cssContent = await collectCss(next)
// Each directory's PostCSS config passes a unique color option.
// Verify every expected color appears in the output.
for (const [, pattern] of Object.entries(DIR_COLORS)) {
expect(cssContent).toMatch(pattern)
}
// No original `color: red` should remain — all were transformed.
expect(cssContent).not.toMatch(/color\s*:\s*red/)
// The old hardcoded green should NOT appear, proving options are used.
expect(cssContent).not.toMatch(/green|#0f0|#008000/)
})
})`
- ID 317: `test/e2e/image-optimizer/image-optimizer.test.ts` —
`describe('Server support for trailingSlash in next.config.js', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'app'),
nextConfig: {
trailingSlash: true,
images: {
imageSizes: [8, 16, 32, 48, 64, 96, 128, 256, 384],
qualities: [70, 75],
},
},
})
it('should return successful response for original loader', async () =>
{
const query = { url: '/test.png', w: 8, q: 70 }
const res = await next.fetch(`/_next/image/?${toQueryString(query)}`)
expect(res.status).toBe(200)
})
})`
- ID 329: `test/e2e/next-image-legacy/default/default-static.test.ts` —
`describe('Static Image Component Tests', () => {
const { next, isTurbopack } = nextTestSetup({
files: __dirname,
})
let browser: Playwright
let html: string
beforeAll(async () => {
html = await next.render('/static-img')
browser = await next.browser('/static-img')
})
it('Should allow an image with a static src to omit height and width',
async () => {
expect(await browser.elementById('basic-static')).toBeTruthy()
expect(await browser.elementById('blur-png')).toBeTruthy()
expect(await browser.elementById('blur-webp')).toBeTruthy()
expect(await browser.elementById('blur-avif')).toBeTruthy()
expect(await browser.elementById('blur-jpg')).toBeTruthy()
expect(await browser.elementById('static-svg')).toBeTruthy()
expect(await browser.elementById('static-gif')).toBeTruthy()
expect(await browser.elementById('static-bmp')).toBeTruthy()
expect(await browser.elementById('static-ico')).toBeTruthy()
expect(await browser.elementById('static-unoptimized')).toBeTruthy()
})
;(isNextStart ? it : it.skip)(
'Should use immutable cache-control header for static import',
async () => {
await browser.eval(
`document.getElementById("basic-static").scrollIntoView()`
)
await new Promise((resolve) => setTimeout(resolve, 1000))
const url = await browser.eval(
`document.getElementById("basic-static").src`
)
const res = await fetch(url)
expect(res.headers.get('cache-control')).toBe(
'public, max-age=315360000, immutable'
)
}
)
;(isNextStart ? it : it.skip)(
'Should use immutable cache-control header even when unoptimized',
async () => {
await browser.eval(
`document.getElementById("static-unoptimized").scrollIntoView()`
)
await new Promise((resolve) => setTimeout(resolve, 1000))
const url = await browser.eval(
`document.getElementById("static-unoptimized").src`
)
const res = await fetch(url)
expect(res.headers.get('cache-control')).toBe(
'public, max-age=31536000, immutable'
)
}
)
it('Should automatically provide an image height and width', async () =>
{
expect(html).toContain('width:400px;height:300px')
})
it('Should allow provided width and height to override intrinsic', async
() => {
expect(html).toContain('width:200px;height:200px')
expect(html).not.toContain('width:400px;height:400px')
})
it('Should add a blur placeholder to statically imported jpg', async ()
=> {
const $ = cheerio.load(html)
const style = $('#basic-static').attr('style')
if (isNextDev && !isTurbopack) {
// In webpack dev, `next/legacy/image` emits a dynamic blur URL via the
// image optimizer route instead of an inlined base64 data URL, to avoid
// slowing down the dev server (see
// `packages/next/src/build/webpack/loaders/next-image-loader/blur.ts`).
expect(replaceBlurUrl(style)).toMatchInlineSnapshot(
`"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0%
0%;filter:blur(20px);background-image:url("<REPLACED_BLUR_URL>")"`
)
} else {
expect(replaceDataUrl(style)).toMatchInlineSnapshot(
`"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0%
0%;filter:blur(20px);background-image:url("data:<REPLACED>")"`
)
}
})
it('Should add a blur placeholder to statically imported png', async ()
=> {
const $ = cheerio.load(html)
const style = $('#basic-static')[2].attribs.style
if (isTurbopack) {
expect(style).toMatchInlineSnapshot(
`"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0%
0%;filter:blur(20px);background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAICAYAAAA870V8AAAARUlEQVR42l3MoQ0AQQhE0XG7xWwIJSBIKBRJOZRBEXOWnPjimQ8AXC3ce+nuPOcQEcHuppkRVcWZYWYSIkJV5XvvN9j4AFZHJTnjDHb/AAAAAElFTkSuQmCC")"`
)
} else if (isNextDev) {
// In webpack dev, `next/legacy/image` emits a dynamic blur URL via the
// image optimizer route instead of an inlined base64 data URL, to avoid
// slowing down the dev server (see
// `packages/next/src/build/webpack/loaders/next-image-loader/blur.ts`).
expect(replaceBlurUrl(style)).toMatchInlineSnapshot(
`"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0%
0%;filter:blur(20px);background-image:url("<REPLACED_BLUR_URL>")"`
)
} else {
// In webpack start, the exact base64 output of the blur placeholder
// depends on the environment's sharp/libvips version, so normalize the
// data URL contents to only assert the data URL prefix.
expect(replaceDataUrl(style)).toMatchInlineSnapshot(
`"position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;background-size:cover;background-position:0%
0%;filter:blur(20px);background-image:url("data:<REPLACED>")"`
)
}
})
it('should load direct imported image', async () => {
const src = await
browser.elementById('basic-static').getAttribute('src')
expect(src).toMatch(
/_next\/image\?url=%2F_next%2Fstatic%2F(immutable%2F)?media%2Ftest-rect(.+)\.jpg&w=828&q=75/
)
const fullSrc = new URL(src, next.url)
const res = await fetch(fullSrc)
expect(res.status).toBe(200)
})
it('should load staticprops imported image', async () => {
const src = await browser
.elementById('basic-staticprop')
.getAttribute('src')
expect(src).toMatch(
/_next\/image\?url=%2F_next%2Fstatic%2F(immutable%2F)?media%2Fexif-rotation(.+)\.jpg&w=256&q=75/
)
const fullSrc = new URL(src, next.url)
const res = await fetch(fullSrc)
expect(res.status).toBe(200)
})
})`
- ID 345: `test/e2e/next-image-svgo-webpack/svgo-webpack.test.ts` —
`describe('svgo-webpack loader', () => {
const { next } = nextTestSetup({
files: __dirname,
dependencies: {
'@svgr/webpack': '8.1.0',
},
})
it('should render an SVG that is transformed by @svgr/webpack into a
React component (pages router)', async () => {
const browser = await next.browser('/pages')
expect(await browser.elementByCss('svg')).toBeDefined()
})
it('should render an SVG that is transformed by @svgr/webpack into a
React component (app router)', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('svg')).toBeDefined()
})
})`
- ID 347: `test/e2e/styled-jsx-dynamic/index.test.ts` —
`describe('styled-jsx dynamic styles SSR', () => {
const { next } = nextTestSetup({
files: __dirname,
})
// Dynamic styled-jsx (with interpolated expressions) produces numeric
class
// names at runtime via the DJB2 hash in styled-jsx's computeId
function.
// This pattern matches production deployments where all jsx class names
// are numeric (e.g. jsx-2267428885) rather than hex
(jsx-f36313d9f07883b7).
it('should contain dynamic styled-jsx styles during SSR', async () => {
const html = await next.render('/')
// Dynamic styled-jsx produces numeric class names at runtime
const numericClasses = html.match(/\bjsx-\d+\b/g) || []
console.log('Numeric jsx classes:', [...new Set(numericClasses)])
expect(numericClasses.length).toBeGreaterThan(0)
// All dynamic styles should be present as inline <style> tags
expect(html).toMatch(/color:.*?green/) // main page
expect(html).toMatch(/color:.*?blue/) // DynamicStyled
expect(html).toMatch(/background-color:.*?navy/) // header
expect(html).toMatch(/color:.*?purple/) // footer
})
})`
- ID 348: `test/e2e/styled-jsx/index.test.ts` — `describe('styled-jsx',
() => {
const { next } = nextTestSetup({
files: __dirname,
dependencies: {
'styled-jsx': '5.0.0', // styled-jsx on user side
},
})
it('should contain styled-jsx styles during SSR', async () => {
const html = await next.render('/')
expect(html).toMatch(/color:.*?red/)
expect(html).toMatch(/color:.*?cyan/)
})
it('should render styles during CSR', async () => {
const browser = await next.browser('/')
const color = await browser.eval(
`getComputedStyle(document.querySelector('button')).color`
)
expect(color).toMatch('0, 255, 255')
})
it('should render styles inside TypeScript', async () => {
const browser = await next.browser('/typescript')
const color = await browser.eval(
`getComputedStyle(document.querySelector('button')).color`
)
expect(color).toMatch('255, 0, 0')
})
})`
</details>
<details>
<summary>Deployment evidence for the additional scopes</summary>
- `test/e2e/image-optimizer/image-optimizer.test.ts` — `describe('Server
support for trailingSlash in next.config.js', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426784928/job/102715630953),
[cache](https://github.com/vercel/next.js/actions/runs/34426784928/job/102715630856).
- `test/e2e/next-image-legacy/default/default-static.test.ts` —
`describe('Static Image Component Tests', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426784928/job/102715630923),
[cache](https://github.com/vercel/next.js/actions/runs/34426784928/job/102715630827).
</details>
<!-- NEXT_JS_LLM -->
|
||
|
|
6ba71046b0 |
[test] Move the harness off node-fetch (#98195)
Node.js ships with a built-in `fetch` now so `node-fetch` is no longer necessary. Mostly motivated by tracing Node.js deprecation warnings which originated from `node-fetch` by calling the deprecated `url.parse`. Call sites keep working through a compatibility type on `fetchViaHTTP` that translates node-fetch-only options: Instead of `agent` we pass to `http(s)` directly, `timeout` becomes `AbortSignal.timeout`, and Node.js readable streams are accepted as bodies with `duplex: 'half'` set automatically. The `abort-controller` polyfill is dropped since its signal type predates the current AbortSignal and undici would not honor it. `node-fetch` stays installed because `scripts/generate-release-log.mjs`, `scripts/reset-project.mjs`, and `scripts/update-google-fonts.js` still import it (follow-up material). Fixture apps will be migrated separately. |
||
|
|
fad317505d |
fix(next/image): reject non-2xx internal image responses (#97957)
- Fixes https://github.com/vercel/next.js/issues/82357 - Closes https://github.com/vercel/next.js/pull/96985 ### What? `fetchInternalImage()` only rejects the internal response when `statusCode` is falsy, and `MockedResponse` defaults it to `200`, so nothing really gets rejected there. A `307` coming from a `redirects()` entry in `next.config`, or a `404` for a path that isn't there, both count as a successful image fetch. That body then reaches `detectContentType()`, which returns `null`, and all you get is: ``` The requested resource isn't a valid image for /path/to/image.png received null ``` Nothing in that line points back at the response that actually came in, so a redirect looks exactly like a corrupt image. ### Why? `fetchExternalImage()` right above already covers this with `if (!res.ok)` and logs `res.status`. The internal path is the only fetch in the file without that check, so the same failure gets reported at the fetch for remote images, and three steps later as "not a valid image" for local ones. ### How? The guard now rejects anything outside the 2xx range, which is what `res.ok` means, and the log line carries the status next to the href. The thrown `ImageError` message stays generic, same as the external path, so nothing new shows up in the HTTP response body. Because that throw passes `mocked.res.statusCode` into `ImageError`, a non-2xx internal response now reports its own status instead of always landing on `400`. `ImageError` still maps anything under 400 to `500`, so a redirect answers `500` and a `404` stays `404`. That is what `fetchExternalImage()` has always done, and it lines the two up: a missing local image now returns `404` just like a missing remote one. Three assertions in `test/e2e/image-optimizer/util.ts` shift because of that: - `should not forward cookie header`: `/api/conditional-cookie` answers `401` when the cookie is missing, so `400` becomes `401` - `should error if the image file does not exist`: `/does_not_exist.jpg` returns `404` now, with the internal response message - `should error if the resource isn't a valid image`: this one requested `/test.txt`, but the fixture in `public/` is `text.txt`, so it was quietly exercising a missing file instead of a non-image file. Pointed at the file that exists, it keeps its original `400` and finally tests what its name says Two unit tests cover the new guard, one `307` and one `404`. Every other test in that file sets `statusCode = 200`, so they are untouched. #96985 is open against the same report with a different angle: it threads the status through `ImageUpstream` so the existing "isn't a valid image" log can print it. This one stops the request at the fetch, so the redirect never reaches the optimizer at all. Whichever one fits better, feel free to close mine. ### Verification - `jest test/unit/image-optimizer/`: 139/139, and the two new tests fail against a build without the guard - `pnpm test-dev test/e2e/image-optimizer/content-disposition-type.test.ts`: 97/97 - `pnpm test-start test/e2e/image-optimizer/content-disposition-type.test.ts`: 97/97 - `prettier --check` clean on the three changed files <!-- NEXT_JS_LLM --> Co-authored-by: Steven <steven@ceriously.com> |
||
|
|
1a7ccf481d |
Re-enable AVIF image optimization (#97931)
Reverts #97875 and bumps `sharp` from `^0.35.3` to `^0.35.4`, re-enabling AVIF image optimization. `minimumReleaseAge` interplay (verified with pnpm 10.33.0 and 11.22.0 in a standalone repo with a `file:` dependency declaring sharp as an optional dependency): - sharp 0.35.4 was published on 2026-08-26 and is younger than this repo's 48h `minimumReleaseAge`. - pnpm aborts the whole install with `ERR_PNPM_NO_MATURE_MATCHING_VERSION` when no in-range release satisfies the age gate. This applies to `optionalDependencies` (no warn-and-skip) and through `file:` dependencies alike. - The gate also covers the `@img/sharp-*` binary packages sharp 0.35.4 depends on, so excluding only `sharp@0.35.4` is not enough. - `pnpm-workspace.yaml` therefore exempts `sharp@0.35.4` and `@img/sharp-*` (pnpm does not allow version qualifiers on name patterns in this setting). Both entries can be dropped once 0.35.4 is older than 48 hours. - Downstream users who configure their own `minimumReleaseAge` will hit the same hard install failure when installing a release with this bump until 0.35.4 ages out, unless they add the same excludes. Keeping `^0.35.3` is not a safer alternative: age-gated installs would then silently resolve the vulnerable 0.35.3. Part of https://linear.app/vercel/issue/VOC-34654/ --------- Co-authored-by: Steven <steven@ceriously.com> |
||
|
|
7a5937a8ab |
[next/image]: disable avif image optimization (#97875)
https://github.com/vercel/next-js-mirror/pull/160 |
||
|
|
7c19c7c06a |
fix(next/image): improve test coverage for unsupported image types (#96301)
We were missing some test coverage for image optimization. This PR adds tests for many missing image formats. Additionally, this PR also ensures the sharp encoders match that allowlist from detectContentType() such that future versions of sharp that enable new encoders by default are not automatically enabled in next/image. Practically, there is no change to the end user because detectContentType() runs before sharp so that is the true allowlist, but this gives us a little more control to align the sharp allowlist as well (since sharp can release new features in semver minor). |
||
|
|
8141dcf12e |
Convert test/integration to isolated tests (#93247)
### What?
Converts every test under `test/integration/` to an isolated test
running through `nextTestSetup` (under `test/e2e/`, `test/production/`,
`test/development/`, or `test/unit/`), then deletes `test/integration/`
along with the legacy CI orchestration that was specific to it.
- `test/integration/` removed entirely (~327 test suites)
- New isolated suites added across the existing folders:
- `test/e2e/` — 175
- `test/production/` — 130
- `test/development/` — 43
- `test/unit/` — 1
- `.github/workflows/build_and_test.yml` and `run-tests.js` no longer
have any `integration` branches
- `nextTestSetup` gained a `baseUrl` option on `next.browser()` so a
small number of tests that drive their own proxy/static-export server
can keep using `next.browser(...)` instead of importing `next-webdriver`
directly
### Why?
`test/integration/` predated `nextTestSetup` and ran tests directly
against the source checkout via custom helpers (`launchApp`,
`nextBuild`, `nextStart`, `runNextCommand`, `webdriver`, `fetchViaHTTP`,
…). Each suite hand-rolled its own dev/start/build orchestration,
fixture mutation, and process management.
The isolated test model used by the rest of the repo gives each suite an
isolated working directory containing a packed `next.tgz` install, a
uniform `next.start()` / `next.build()` / `next.fetch()` /
`next.browser()` API, and the same lifecycle for dev, start, and deploy
modes — so a single set of assertions covers all three. Deploy-mode
skips and per-feature gates are expressed declaratively
(`skipDeployment`, `disableAutoSkewProtection`, `if (skipped) return`)
instead of branching on `process.env`.
Removing `test/integration/` lets us:
- Delete the bespoke orchestration code in the CI workflow and
`run-tests.js`
- Run every converted suite consistently in dev, start, and deploy modes
(where applicable)
- Reproduce every test locally with the same `pnpm
test-{dev,start}-{turbo,webpack}` commands; no separate `integration`
path
- Open the door to running `test/production` against deployments in the
future (the converted suites already declare `skipDeployment` so they
can be flipped on)
### How?
Mechanical conversion per suite, with targeted clean-ups:
1. **Per-suite conversion.** Each
`test/integration/<name>/test/index.test.{js,ts}` was rewritten into a
single `<name>.test.ts` under the right folder based on what the
original exercised:
- `launchApp` / dev-only assertions → `test/development/`
- `nextBuild` + `nextStart` / start-only assertions → `test/production/`
- Both → `test/e2e/`
- The one pure jsdom render check (`link-without-router`) → `test/unit/`
2. **API mapping.** Custom helpers were replaced by `nextTestSetup`
equivalents: `launchApp` → `next.start()`, `nextBuild` → `next.build()`,
`runNextCommand` → `next.runCommand`, `fetchViaHTTP` → `next.fetch`,
`webdriver(...)` → `next.browser(...)`. Fixture mutations switched from
raw `fs.writeFile`/`fs.rename` to `next.patchFile` (with the 3-arg
`runWithTempContent` callback when the change has a defined scope) and
`next.deleteFile`.
3. **Deploy-mode handling.** Suites that can't run in deploy mode (use
`patchFile` / `next.build()` / depend on local CLI output) declare
`skipDeployment: true` and early-return on the `skipped` boolean. Suites
where Vercel's edge mutates URLs (`&dpl=`, immutable assets) declare
`disableAutoSkewProtection: true`.
4. **`next.browser({ baseUrl })`.** A handful of tests
(`prerender-export`, `cdn-cache-busting`, `preload-viewport`, both
`react-virtualized` suites) need to drive a separate server (a
static-export server or an `http-proxy` instance) rather than the
Next.js process. Instead of importing `next-webdriver` directly, those
tests now pass `{ baseUrl: <port|url> }` to `next.browser()`. For the
proxy cases, the proxy was moved into `server.js` inside the fixture and
`http-proxy` declared via the `dependencies` option of `nextTestSetup`,
so the test runs with a fully isolated dependency graph.
5. **CI clean-up.** With `test/integration` gone, the `test
integration*` jobs and `integration-tests-manifest`-related logic in
`.github/workflows/build_and_test.yml` were removed, and `run-tests.js`
no longer has the `integration` test-folder branch.
6. **Validation.** The PR was iterated against multiple full CI runs;
the remaining failures on the latest run are pre-existing flakes
(segment-cache 60s `act` timeouts in turbopack-prod) or transient
infrastructure issues unrelated to the conversion.
|