Skip to content
Draft
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
104 changes: 103 additions & 1 deletion __tests__/e2e/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import path from 'node:path'
import { defineConfig, type DefaultTheme } from 'vitepress'

let renderCapturedMarkdown: (() => Promise<string>) | undefined
const batchHeadHookPages = new Set<string>()
let batchHeadHookSequence = 0

const nav: DefaultTheme.Config['nav'] = [
{
text: 'Home',
Expand Down Expand Up @@ -154,8 +159,15 @@ const sidebar: DefaultTheme.Config['sidebar'] = {
export default defineConfig({
title: 'Example',
description: 'An example app using VitePress.',
srcExclude: process.env.VITE_TEST_SSR_BATCH ? [] : ['ssr-*.md'],
ssrBuildBatchSize: process.env.VITE_TEST_SSR_BATCH ? 10 : undefined,
ssrBuildWorkerConcurrency: process.env.VITE_TEST_SSR_BATCH ? 2 : undefined,
markdown: {
image: { lazyLoad: true }
image: { lazyLoad: true },
config(md) {
renderCapturedMarkdown = () =>
md.renderAsync('```ts\nconst batch = true\n```')
}
},
themeConfig: {
nav,
Expand All @@ -181,11 +193,101 @@ export default defineConfig({
}
},
vite: {
build: {
// Test the batching guard. It prevents SSR workers from copying the
// public directory into temporary output.
copyPublicDir: true
},
plugins: [
{
name: 'test:ssr-batch-public-copy',
config() {
if (process.env.VITE_TEST_SSR_BATCH) {
return {
publicDir: 'batch-public',
resolve: {
alias: {
'/vitepress.png': path.resolve(
import.meta.dirname,
'../public/vitepress.png'
)
}
},
environments: {
ssr: { build: { copyPublicDir: true } }
}
}
}
},
configResolved(config) {
if (
process.env.VITE_TEST_SSR_BATCH &&
config.build.ssr &&
(config.build.copyPublicDir !== false ||
config.environments.ssr?.build.copyPublicDir !== false)
) {
throw new Error('SSR batch worker would copy the public directory')
}
}
}
],
server: {
watch: {
usePolling: true,
interval: 100
}
}
},
buildEnd(siteConfig) {
if (
process.env.VITE_TEST_SSR_BATCH &&
siteConfig.publicDir !== path.resolve(siteConfig.srcDir, 'batch-public')
) {
throw new Error('Resolved publicDir was not restored in the coordinator')
}
if (
process.env.VITE_TEST_SSR_BATCH &&
(!batchHeadHookPages.has('ssr-static.md') ||
!batchHeadHookPages.has('dynamic-routes/foo.md'))
) {
throw new Error(
'Coordinator-owned build hook state was not preserved across SSR workers'
)
}
},
transformHead(context) {
if (!process.env.VITE_TEST_SSR_BATCH) return
batchHeadHookPages.add(context.page)
return [
[
'meta',
{
name: 'ssr-batch-hook-state',
content: `${++batchHeadHookSequence}:${context.pageData.relativePath}`
}
]
]
},
transformHtml(code, _id, context) {
if (!process.env.VITE_TEST_SSR_BATCH) return
if (!batchHeadHookPages.has(context.page)) {
throw new Error(
'transformHtml ran without coordinator transformHead state'
)
}

return code.replace(
'</body>',
`<span hidden data-ssr-batch-transform="${context.page}"></span>\n </body>`
)
},
async postRender(context) {
if (process.env.VITE_TEST_SSR_BATCH) {
if (!renderCapturedMarkdown) {
throw new Error('Markdown renderer was not captured during SSR setup')
}
await renderCapturedMarkdown()
}
return context
}
})
1 change: 1 addition & 0 deletions __tests__/e2e/batch-public/batch-public.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
copied once by the client build
1 change: 1 addition & 0 deletions __tests__/e2e/dynamic-routes/dynamic-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ describe('dynamic routes', () => {
await goto('/dynamic-routes/foo')
expect(await page.textContent('h1')).toMatch('Foo')
expect(await page.textContent('pre.params')).toMatch('"id": "foo"')
expect(await page.title()).toBe('Foo - transformed | Example')

await goto('/dynamic-routes/bar')
expect(await page.textContent('h1')).toMatch('Bar')
Expand Down
23 changes: 23 additions & 0 deletions __tests__/e2e/local-search/local-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@ describe('local search', () => {
).toBe(0)
})

test.runIf(process.env.VITE_TEST_SSR_BATCH)(
'indexes page HTML produced by the artifact pipeline',
async () => {
await page.locator('.VPNavBarSearchButton').click()

const input = await page.waitForSelector('input#localsearch-input')
await input.type('Static HTML marker')

await page.waitForFunction(() =>
[
...document.querySelectorAll('#localsearch-list li[role=option]')
].some((option) => option.textContent?.includes('Static batching page'))
)

expect(
await page
.locator('#localsearch-list li[role=option]')
.filter({ hasText: 'Static batching page' })
.count()
).toBeGreaterThan(0)
}
)

test('uses the same desktop breakpoint as the nav bar', async () => {
try {
for (const { width, isDesktop } of [
Expand Down
120 changes: 120 additions & 0 deletions __tests__/e2e/ssr-batching.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { access, readFile } from 'node:fs/promises'
import path from 'node:path'

test('batched SSR writes complete shared and per-page artifacts', async () => {
if (!process.env.VITE_TEST_SSR_BATCH) return

const outDir = path.resolve('.vitepress/dist')
const [
indexHtml,
dynamicHtml,
staticHtml,
scopedHtml,
lastBatchHtml,
notFoundHtml,
iconsCss,
hashmap
] = await Promise.all([
readFile(path.join(outDir, 'index.html'), 'utf8'),
readFile(path.join(outDir, 'dynamic-routes/foo.html'), 'utf8'),
readFile(path.join(outDir, 'ssr-static.html'), 'utf8'),
readFile(path.join(outDir, 'ssr-scoped.html'), 'utf8'),
readFile(path.join(outDir, 'text-literals/index.html'), 'utf8'),
readFile(path.join(outDir, '404.html'), 'utf8'),
readFile(path.join(outDir, 'vp-icons.css'), 'utf8'),
readFile(path.join(outDir, 'hashmap.json'), 'utf8')
])

expect(indexHtml).toContain('<div id="app">')
expect(dynamicHtml).toContain('<title>Foo - transformed | Example</title>')
expect(dynamicHtml).toContain('name="ssr-batch-hook-state"')
expect(dynamicHtml).toContain(
'data-ssr-batch-transform="dynamic-routes/foo.md"'
)
expect(staticHtml).toContain('<title>Static batching page | Example</title>')
expect(staticHtml).toContain('<h1 id="static-batching-page"')
expect(staticHtml).toContain(
'<p data-static-batch-marker="preserved">Static HTML marker</p>'
)
expect(staticHtml).toContain(
'<span class="VPBadge warning">static badge</span>'
)
expect(staticHtml).toContain('<!-- static comment preserved -->')
expect(staticHtml).toContain(
'<img data-static-public-asset src="/batch-public.txt" alt="Static public asset">'
)
expect(scopedHtml).toContain('Scoped module identity')
expect(scopedHtml).toMatch(/class="scoped-batch-marker" data-v-[\da-f]+/)
expect(staticHtml).toMatch(
/<meta name="ssr-batch-hook-state" content="\d+:ssr-static\.md">/
)
expect(staticHtml).toContain(
'<meta name="ssr-batch-after-config-resolve" content="coordinator mutation retained">'
)
expect(staticHtml).toContain('data-ssr-batch-transform="ssr-static.md"')
expect(lastBatchHtml).toContain('<h1 id="text-literals"')
expect(dynamicHtml).toContain('<pre class="params">')
expect(dynamicHtml).toContain('&quot;id&quot;: &quot;foo&quot;')
expect(dynamicHtml).not.toContain('{{ $params }}')
expect(notFoundHtml).toContain('<title>404 | Example</title>')
expect(iconsCss).toContain('.vpi-social-github')
expect(hashmap).not.toContain('undefined')
await expect(
access(path.join(outDir, 'batch-public.txt'))
).resolves.toBeUndefined()
if (!process.env.DEBUG) {
await expect(access(path.resolve('.vitepress/.temp'))).rejects.toThrow()
}
})

test('resolved config-file hooks preserve legacy physical Markdown SSR semantics', async () => {
if (!process.env.VITE_TEST_BUILD) return

const html = await readFile(
path.resolve('.vitepress/dist/ssr-plugin-safety.html'),
'utf8'
)
expect(html).toContain(
'<p data-resolved-load-environment="ssr">physical Markdown load hook</p>'
)
expect(html).toContain(
'<p data-resolved-transform-mode="server">environment-sensitive Markdown transform</p>'
)
expect(html).toContain(
'<p data-resolved-plugin-context="build:false">production plugin context</p>'
)
expect(html).not.toContain('data-resolved-transform-mode="client"')
})

test('a batched SSR page hydrates with normal client-page semantics', async () => {
if (!process.env.VITE_TEST_SSR_BATCH) return

await goto('/ssr-static.html')

expect(
await page
.getByRole('heading', { level: 1, name: 'Static batching page' })
.isVisible()
).toBe(true)
expect(
await page.locator('[data-static-batch-marker="preserved"]').textContent()
).toBe('Static HTML marker')
expect(await page.locator('.VPBadge.warning').textContent()).toBe(
'static badge'
)
expect(
await page.locator('[data-static-public-asset]').getAttribute('src')
).toBe('/batch-public.txt')
})

test('scoped pages preserve client and SSR module identity', async () => {
if (!process.env.VITE_TEST_SSR_BATCH) return

await goto('/ssr-scoped.html')

const marker = page.locator('.scoped-batch-marker')
expect(await marker.textContent()).toBe('Scoped module identity')
expect(
await marker.evaluate((element) => getComputedStyle(element).color)
).toBe('rgb(1, 2, 3)')
})
7 changes: 7 additions & 0 deletions __tests__/e2e/ssr-plugin-safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Resolved plugin artifact safety
---

# Resolved plugin artifact safety

This page is transformed by a plugin loaded from `vite.config.ts`.
14 changes: 14 additions & 0 deletions __tests__/e2e/ssr-scoped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: Scoped batching page
description: A page that must preserve its physical Markdown module identity.
---

# Scoped batching page

<div class="scoped-batch-marker">Scoped module identity</div>

<style scoped>
.scoped-batch-marker {
color: rgb(1, 2, 3);
}
</style>
18 changes: 18 additions & 0 deletions __tests__/e2e/ssr-static.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: Static batching page
description: A page used to verify batched SSR output.
---

# Static batching page

This content is rendered without evaluating a per-page SSR module.

<p data-static-batch-marker="preserved">Static HTML marker</p>

## Static presentational markup

<span class="VPBadge warning">static badge</span>

<!-- static comment preserved -->

<img data-static-public-asset src="/batch-public.txt" alt="Static public asset">
49 changes: 49 additions & 0 deletions __tests__/e2e/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { defineConfig } from 'vite'

const artifactSafetyPageRE = /(?:^|\/)ssr-plugin-safety[.]md$/

export default defineConfig({
publicDir: process.env.VITE_TEST_SSR_PLUGIN_PARITY
? 'batch-public'
: undefined,
resolve: process.env.VITE_TEST_SSR_PLUGIN_PARITY
? {
alias: {
'/vitepress.png': path.resolve(
import.meta.dirname,
'public/vitepress.png'
)
}
}
: undefined,
plugins: [
{
name: 'test:config-file-artifact-safety',
apply: 'build',
applyToEnvironment(environment) {
const environmentName = environment.name
return {
name: `test:resolved-artifact-safety:${environmentName}`,
enforce: 'pre',
load: {
filter: { id: artifactSafetyPageRE },
async handler(id) {
const source = await readFile(id, 'utf8')
return `${source}\n<p data-resolved-load-environment="${environmentName}">physical Markdown load hook</p>`
}
},
transform: {
filter: { id: artifactSafetyPageRE },
handler(code, _id, options) {
const mode = options?.ssr ? 'server' : 'client'
const pluginContext = `${this.environment.mode}:${this.meta.watchMode}`
return `${code}\n<p data-resolved-transform-mode="${mode}">environment-sensitive Markdown transform</p>\n<p data-resolved-plugin-context="${pluginContext}">production plugin context</p>`
}
}
}
}
}
]
})
Loading