Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 74 additions & 67 deletions packages/unplugin-skew-protection/test/e2e/browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
import { chromium, type Browser } from 'playwright'

import { BUNDLERS, TOKEN, createFixture } from '../support/builders.js'
import { BUNDLERS, PAGES, TOKEN, createFixture } from '../support/builders.js'
import { serveStatic, type StaticServer } from '../support/serve.js'

const EXPECTED_QUERY = `?nfdpl=${TOKEN}`
Expand Down Expand Up @@ -35,75 +35,82 @@
await rm(root, { force: true, recursive: true })
})

test('serves an app whose asset requests are pinned to the deploy', async () => {
const page = await browser.newPage()
test('serves pages whose asset requests are pinned to the deploy', async () => {
const assetRequests: URL[] = []
const failedResponses: string[] = []

page.on('request', (request) => {
const url = new URL(request.url())

if (/\.(css|js|mjs)$/.test(url.pathname)) {
assetRequests.push(url)
const rendered: { path: string; text: string | null }[] = []

for (const { path } of PAGES) {
// Each document gets a browser page of its own: `newPage` opens a fresh context, so the chunk
// the two documents share is requested again on the second visit instead of being served out
// of the first visit's cache, where it would never be observed at all.
const page = await browser.newPage()

page.on('request', (request) => {
const url = new URL(request.url())

if (/\.(css|js|mjs)$/.test(url.pathname)) {
assetRequests.push(url)
}
})

page.on('response', (response) => {
// The browser asks for a favicon that the fixture does not ship; every other 4xx/5xx
// means a stamped URL failed to resolve, which is the failure mode worth catching.
if (response.status() >= 400 && !response.url().endsWith('/favicon.ico')) {
failedResponses.push(`${path} -> ${String(response.status())} ${response.url()}`)
}
})

try {
await page.goto(`${server.url}${path}`)

// The fixture marks `data-state` on both the success and failure paths, so a chunk that
// never loads leaves the text at "<name> loading" for the comparison below to report,
// rather than stalling the whole test -- which keeps every page and assertion in play and
// shows all the problems in one go.
await page.waitForSelector('#app[data-state]', { timeout: 10_000 }).catch(() => null)

rendered.push({ path, text: await page.textContent('#app') })
} finally {
await page.close()
}
})

page.on('response', (response) => {
// The browser asks for a favicon that the fixture does not ship; every other 4xx/5xx
// means a stamped URL failed to resolve, which is the failure mode worth catching.
if (response.status() >= 400 && !response.url().endsWith('/favicon.ico')) {
failedResponses.push(`${String(response.status())} ${response.url()}`)
}
})

try {
await page.goto(`${server.url}/`)

// The fixture marks `data-state` on both the success and failure paths, so a chunk that
// never loads is reported as a soft failure here instead of stalling the whole test --
// which keeps the assertions below running and shows every problem in one go.
const reachedTerminalState = await page
.waitForSelector('#app[data-state]', { timeout: 10_000 })
.then(() => true)
.catch(() => false)

expect
.soft(reachedTerminalState, 'the app never finished loading: its dynamic import neither resolved nor rejected')
.toBe(true)

expect
.soft(
failedResponses,
'the page requested assets that the server could not serve, so a stamped URL does not point at a file this build emitted',
)
.toEqual([])

expect
.soft(
await page.textContent('#app'),
'the lazily imported chunk did not evaluate in the browser, so its stamped specifier does not resolve to a working module',
)
.toBe('lazy chunk loaded')

const unstamped = assetRequests.filter((url) => url.search !== EXPECTED_QUERY).map((url) => url.pathname)
expect
.soft(
unstamped,
'these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy',
)
.toEqual(expectedUnstamped)

// Guards against a vacuous pass: with no asset requests at all, the comparison above is
// satisfied by an empty list for the bundlers that are expected to stamp everything.
const stamped = assetRequests.filter((url) => url.search === EXPECTED_QUERY).map((url) => url.pathname)
expect
.soft(
stamped.length,
`no asset was requested with the deploy-pinning query parameter (all requests: ${assetRequests.map((url) => url.pathname).join(', ') || 'none'})`,
)
.toBeGreaterThan(0)
} finally {
await page.close()
}

expect
.soft(
failedResponses,
'these pages requested assets that the server could not serve, so a stamped URL does not point at a file this build emitted',
)
.toEqual([])

expect
.soft(
rendered,
'these pages did not settle with both of their shared chunks evaluated, so a stamped specifier does not resolve to a working module',
)
.toEqual(PAGES.map(({ path, text }) => ({ path, text })))

// Deduplicated because the two pages request the chunk they share separately, and sorted so the
// comparison does not depend on the order the browser happened to fetch things in.
const unstamped = [
...new Set(assetRequests.filter((url) => url.search !== EXPECTED_QUERY).map((url) => url.pathname)),
].sort()
expect
.soft(
unstamped,
'these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy',
)
.toEqual(expectedUnstamped)

Check failure on line 104 in packages/unplugin-skew-protection/test/e2e/browser.test.ts

View workflow job for this annotation

GitHub Actions / test (windows-latest, 24, 24.15.0)

test/e2e/browser.test.ts > 'rolldown' > serves pages whose asset requests are pinned to the deploy

AssertionError: these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy: expected [ '/shared-static.js' ] to deeply equal [] - Expected + Received - [] + [ + "/shared-static.js", + ] ❯ test/e2e/browser.test.ts:104:8

Check failure on line 104 in packages/unplugin-skew-protection/test/e2e/browser.test.ts

View workflow job for this annotation

GitHub Actions / test (windows-latest, 24, 24.15.0)

test/e2e/browser.test.ts > 'rollup' > serves pages whose asset requests are pinned to the deploy

AssertionError: these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy: expected [ '/shared-static.js' ] to deeply equal [] - Expected + Received - [] + [ + "/shared-static.js", + ] ❯ test/e2e/browser.test.ts:104:8

Check failure on line 104 in packages/unplugin-skew-protection/test/e2e/browser.test.ts

View workflow job for this annotation

GitHub Actions / test (windows-latest, 24, 24.15.0)

test/e2e/browser.test.ts > 'vite' > serves pages whose asset requests are pinned to the deploy

AssertionError: these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy: expected [ Array(1) ] to deeply equal [] - Expected + Received - [] + [ + "/assets/preload-helper-DI4a1MXl.js", + ] ❯ test/e2e/browser.test.ts:104:8

Check failure on line 104 in packages/unplugin-skew-protection/test/e2e/browser.test.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 24, 24.15.0)

test/e2e/browser.test.ts > 'rolldown' > serves pages whose asset requests are pinned to the deploy

AssertionError: these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy: expected [ '/shared-static.js' ] to deeply equal [] - Expected + Received - [] + [ + "/shared-static.js", + ] ❯ test/e2e/browser.test.ts:104:8

Check failure on line 104 in packages/unplugin-skew-protection/test/e2e/browser.test.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 24, 24.15.0)

test/e2e/browser.test.ts > 'rollup' > serves pages whose asset requests are pinned to the deploy

AssertionError: these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy: expected [ '/shared-static.js' ] to deeply equal [] - Expected + Received - [] + [ + "/shared-static.js", + ] ❯ test/e2e/browser.test.ts:104:8

Check failure on line 104 in packages/unplugin-skew-protection/test/e2e/browser.test.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 24, 24.15.0)

test/e2e/browser.test.ts > 'vite' > serves pages whose asset requests are pinned to the deploy

AssertionError: these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy: expected [ Array(1) ] to deeply equal [] - Expected + Received - [] + [ + "/assets/preload-helper-DI4a1MXl.js", + ] ❯ test/e2e/browser.test.ts:104:8

Check failure on line 104 in packages/unplugin-skew-protection/test/e2e/browser.test.ts

View workflow job for this annotation

GitHub Actions / test (macOS-latest, 24, 24.15.0)

test/e2e/browser.test.ts > 'rolldown' > serves pages whose asset requests are pinned to the deploy

AssertionError: these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy: expected [ '/shared-static.js' ] to deeply equal [] - Expected + Received - [] + [ + "/shared-static.js", + ] ❯ test/e2e/browser.test.ts:104:8

Check failure on line 104 in packages/unplugin-skew-protection/test/e2e/browser.test.ts

View workflow job for this annotation

GitHub Actions / test (macOS-latest, 24, 24.15.0)

test/e2e/browser.test.ts > 'rollup' > serves pages whose asset requests are pinned to the deploy

AssertionError: these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy: expected [ '/shared-static.js' ] to deeply equal [] - Expected + Received - [] + [ + "/shared-static.js", + ] ❯ test/e2e/browser.test.ts:104:8

Check failure on line 104 in packages/unplugin-skew-protection/test/e2e/browser.test.ts

View workflow job for this annotation

GitHub Actions / test (macOS-latest, 24, 24.15.0)

test/e2e/browser.test.ts > 'vite' > serves pages whose asset requests are pinned to the deploy

AssertionError: these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy: expected [ Array(1) ] to deeply equal [] - Expected + Received - [] + [ + "/assets/preload-helper-DI4a1MXl.js", + ] ❯ test/e2e/browser.test.ts:104:8

Check failure on line 104 in packages/unplugin-skew-protection/test/e2e/browser.test.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 20.19.0, 20.19.0)

test/e2e/browser.test.ts > 'rolldown' > serves pages whose asset requests are pinned to the deploy

AssertionError: these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy: expected [ '/shared-static.js' ] to deeply equal [] - Expected + Received - [] + [ + "/shared-static.js", + ] ❯ test/e2e/browser.test.ts:104:8

Check failure on line 104 in packages/unplugin-skew-protection/test/e2e/browser.test.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 20.19.0, 20.19.0)

test/e2e/browser.test.ts > 'rollup' > serves pages whose asset requests are pinned to the deploy

AssertionError: these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy: expected [ '/shared-static.js' ] to deeply equal [] - Expected + Received - [] + [ + "/shared-static.js", + ] ❯ test/e2e/browser.test.ts:104:8

Check failure on line 104 in packages/unplugin-skew-protection/test/e2e/browser.test.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 20.19.0, 20.19.0)

test/e2e/browser.test.ts > 'vite' > serves pages whose asset requests are pinned to the deploy

AssertionError: these assets were requested without the deploy-pinning query parameter, so they are not pinned to this deploy: expected [ Array(1) ] to deeply equal [] - Expected + Received - [] + [ + "/assets/preload-helper-DI4a1MXl.js", + ] ❯ test/e2e/browser.test.ts:104:8

// Guards against a vacuous pass: with no asset requests at all, the comparison above is
// satisfied by an empty list for the bundlers that are expected to stamp everything.
const stamped = assetRequests.filter((url) => url.search === EXPECTED_QUERY)
expect
.soft(
stamped.length,
`no asset was requested with the deploy-pinning query parameter (all requests: ${assetRequests.map((url) => url.pathname).join(', ') || 'none'})`,
)
.toBeGreaterThan(0)
})
})
21 changes: 0 additions & 21 deletions packages/unplugin-skew-protection/test/fixtures/entry.js

This file was deleted.

4 changes: 2 additions & 2 deletions packages/unplugin-skew-protection/test/fixtures/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>skew protection e2e</title>
<title>skew protection e2e - index</title>
</head>
<body>
<div id="app">loading</div>
<script type="module" src="./entry.js"></script>
<script type="module" src="./index.js"></script>
</body>
</html>
3 changes: 3 additions & 0 deletions packages/unplugin-skew-protection/test/fixtures/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { renderPage } from './shared-static.js'

renderPage('index', () => import('./shared-dynamic.js'))
1 change: 0 additions & 1 deletion packages/unplugin-skew-protection/test/fixtures/lazy.js

This file was deleted.

11 changes: 11 additions & 0 deletions packages/unplugin-skew-protection/test/fixtures/second.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>skew protection e2e - second</title>
</head>
<body>
<div id="app">loading</div>
<script type="module" src="./second.js"></script>
</body>
</html>
3 changes: 3 additions & 0 deletions packages/unplugin-skew-protection/test/fixtures/second.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { renderPage } from './shared-static.js'

renderPage('second', () => import('./shared-dynamic.js'))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'shared dynamic chunk loaded'
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Statically imported by both entries, so every bundler under test hoists this module into a chunk
// that the two pages share -- a request that has to carry the deploy-pinning parameter just like an
// entry or a dynamically imported chunk does.
const SHARED_STATIC = 'shared static chunk loaded'

/**
* Renders one fixture page. Both entries go through here so the browser test can treat every page
* and every bundler alike: find or create the `#app` mount point, then settle it to a terminal
* `data-state` once the page's dynamic chunk has either resolved or rejected.
*
* `importDynamic` is a callback rather than a specifier because bundlers only rewrite literal
* `import()` specifiers: passing the specifier in would leave a single call site here in the
* shared chunk instead of one per page.
*/
export function renderPage(name, importDynamic) {
// Bundlers that generate their own HTML may not carry the fixture's markup, so the mount point
// is created here when the page does not already provide one.
let app = document.querySelector('#app')

if (!app) {
app = document.createElement('div')
app.id = 'app'
document.body.prepend(app)
}

app.textContent = `${name} loading`

importDynamic()
.then(({ default: sharedDynamic }) => {
app.textContent = `${name}: ${SHARED_STATIC}, ${sharedDynamic}`
app.dataset.state = 'loaded'
})
.catch((error) => {
app.textContent = `failed: ${error.message}`
app.dataset.state = 'failed'
})
}
Loading
Loading