ファイル
btrc-hub/frontend/server.ssr.test.mjs
T
2026-10-20 12:38:36 +09:00

663 行
31 KiB
JavaScript
Raw Blame 履歴

このファイルには不可視のUnicode文字が含まれています
このファイルには人間が識別できない不可視のUnicode文字が含まれており、コンピューターによって特殊な処理が行われる可能性があります。 それが意図的なものと考えられる場合は、この警告を無視して構いません。 不可視文字を表示するにはエスケープボタンを使用します。
import assert from 'node:assert/strict'
import { createServer } from 'node:http'
import { spawn } from 'node:child_process'
import { after, before, test } from 'node:test'
import { createServer as createViteServer } from 'vite'
import { render } from './dist/server/entry-server.js'
const origin = 'https://ssr.example.invalid'
const date = '2026-09-15T00:00:00+09:00'
let api, apiURL, routes, vite, node, httpOrigin
let failAPI = false
const requests = []
// Rails owns encryption and verification; this upstream fixture tests Node's forwarding contract.
const recoveryCode = 'test-recovery-secret'
const users = new Map ([
[recoveryCode, { id: 1, name: 'SSR account', role: 'admin' }],
['second-recovery-secret', { id: 2, name: 'Second account', role: 'member' }]])
const sessions = new Map ()
let sessionSequence = 0
const cookieName = '__Host-btrc-session'
const cookieFor = value => `${cookieName}=${value}`
const sessionValue = request => request.headers.cookie?.split (';')
.map (part => part.trim ()).find (part => part.startsWith (`${cookieName}=`))
?.slice (cookieName.length + 1)
const issueSession = (response, code = null) => {
const value = `opaque-rails-session-${++sessionSequence}`
sessions.set (value, { code, token: `csrf-${sessionSequence}` })
refreshCookie (response, value)
return value
}
const refreshCookie = (response, value) => response.setHeader ('Set-Cookie',
`${cookieFor (value)}; Path=/; Max-Age=630720000; HttpOnly; Secure; SameSite=Lax`)
const tag = locale => ({
id: 1, name: locale === 'ja' ? 'SSR タグ' : 'SSR シロシ', category: 'general',
deprecated_at: null, aliases: [], parents: [], children: [], post_count: 1,
created_at: date, updated_at: date, has_wiki: true, material_id: null,
has_deerjikists: false, sections: [] })
const post = locale => ({
id: 1, version_no: 1, title: `SSR post ${locale}`, url: 'https://example.invalid/post',
thumbnail: null, thumbnail_base: null, video_ms: null, tags: [tag (locale)],
parent_posts: [], child_posts: [], sibling_posts: {}, viewed: false, related: [],
original_created_from: date, original_created_before: date,
created_at: date, updated_at: date, uploaded_user: null })
const wiki = locale => ({
id: 1, tag_id: 1, resource_type: 'tag_description', language_code: locale === 'ja'
? 'ja' : 'ain', title: tag (locale).name, body: `SSR wiki body ${locale}`,
created_at: date, updated_at: date, revision_id: 1, pred: null, succ: null,
deprecated_at: null })
const material = locale => ({
id: 1, version_no: 1, tag: tag (locale), file: null, url: 'https://example.invalid',
thumbnail: null, thumbnail_fallback_text: 'SSR material',
thumbnail_fallback_kind: 'tag_name', media_kind: 'url_only', content_type: null,
file_byte_size: null, export_paths: {}, export_items: [], created_at: date,
updated_at: date, created_by_user: { id: 1, name: 'SSR' },
updated_by_user: { id: 1, name: 'SSR' } })
const responseFor = url => {
const locale = url.searchParams.get ('locale') ?? 'ja'
const path = url.pathname
if (path === '/locales')
return { locales: [{ code: 'ja', name: '日本語' }, { code: 'ain-Kana', name: 'アイヌ' }] }
if (path === '/users/settings') return { theme: 'system', wiki_editor_mode: 'split' }
if (path === '/users/theme_slots') return []
if (path === '/posts') return { posts: [post (locale)], count: 1 }
if (path === '/gekanator/posts') return { posts: [post (locale)] }
if (path === '/gekanator/questions') return { questions: [] }
if (path === '/posts/1') return post (locale)
if (path === '/posts/2')
return { ...post (locale), id: 2,
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' }
if (path === '/tags') return { tags: [tag (locale)], count: 1 }
if (path === '/tags/1' || path.startsWith ('/tags/name/')) return tag (locale)
if (path === '/tags/1/deerjikists') return { tag: tag (locale), deerjikists: [] }
if (path === '/tags/with-depth') return []
if (path === '/tags/nico' || path === '/nico/tags') return { tags: [], count: 0 }
if (path.endsWith ('/versions')) return { versions: [], count: 0 }
if (path === '/wiki') return [wiki (locale)]
if (path === '/wiki/changes') return []
if (path.includes ('autolink')) return []
if (path === '/wiki/1/diff') return { title: 'SSR wiki diff', diff: [] }
if (path.startsWith ('/wiki/')) return wiki (locale)
if (path === '/materials') return { materials: [material (locale)], count: 1 }
if (path === '/materials/1') return material (locale)
if (path.includes ('suppressions')) return { suppressions: [] }
if (path === '/theatres/1') return { id: 1, name: 'SSR theatre', created_at: date }
if (path === '/theatres/1/programmes') return []
if (path === '/theatres/1/comments') return []
if (path === '/theatres/1/post_selection_weights')
return { tag_penalties: [], lightest_posts: [], heaviest_posts: [] }
if (path === '/theatres/1/watching')
return { host_flg: false, post_id: null, post_started_at: null, post_elapsed_ms: null,
watching_users: [], skip_vote: {
votes_count: 0, required_count: 1, watching_users_count: 0, voted: false } }
return null
}
// Give startup failures a bounded, visible error instead of waiting indefinitely.
const withTimeout = async (label, promise, milliseconds) => {
let timer
try {
return await Promise.race ([promise, new Promise ((_, reject) => {
timer = setTimeout (() => reject (new Error (
`[SSR TEST] ${label} timed out after ${milliseconds}ms`)), milliseconds)
})])
} finally {
clearTimeout (timer)
}
}
before (async () => {
console.error('[SSR TEST] setup started')
api = createServer (async (request, response) => {
requests.push ({ method: request.method, url: request.url,
cookie: request.headers.cookie, csrf: request.headers['x-csrf-token'],
transferCode: request.headers['x-transfer-code'] })
const url = new URL (request.url, 'http://localhost')
response.setHeader ('Content-Type', 'application/json')
response.setHeader ('Cache-Control', 'private, no-store')
if (failAPI) {
response.writeHead (503).end ()
return
}
if (url.pathname.startsWith ('/users')) {
const value = sessionValue (request)
const session = sessions.get (value)
const user = users.get (session?.code)
if (request.method === 'GET') {
if (url.pathname === '/users/csrf') {
const id = session != null ? value : issueSession (response)
response.end (JSON.stringify ({ token: sessions.get (id).token }))
return
}
if (user == null) {
response.writeHead (401).end ()
return
}
refreshCookie (response, value)
const data = url.pathname === '/users/session' ? user
: (url.pathname === '/users/me' ? { ...user, inheritance_code: session.code }
: responseFor (url))
response.writeHead (data == null ? 404 : 200)
response.end (JSON.stringify (data))
return
}
if (![origin, httpOrigin].includes (request.headers.origin)
|| session == null || request.headers['x-csrf-token'] !== session.token) {
response.writeHead (403, { 'X-CSRF-Invalid': '1' }).end ()
return
}
let body = ''
for await (const chunk of request) body += chunk
const params = JSON.parse (body || '{}')
if (request.method === 'POST' && url.pathname === '/users/verify') {
const code = params.restore && user != null ? session.code : params.code
const verified = users.get (code)
if (verified == null) {
response.end (JSON.stringify ({ valid: false }))
return
}
sessions.delete (value)
issueSession (response, code)
response.end (JSON.stringify ({ valid: true,
user: { ...verified, inheritance_code: code } }))
return
}
if (request.method === 'DELETE' && url.pathname === '/users/session') {
sessions.delete (value)
response.writeHead (204, { 'Set-Cookie':
`${cookieName}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax` }).end ()
return
}
if (request.method === 'POST' && url.pathname === '/users/code/renew' && user != null) {
const code = `rotated-recovery-${sessionSequence}`
users.delete (session.code)
users.set (code, user)
sessions.delete (value)
issueSession (response, code)
response.end (JSON.stringify ({ code }))
return
}
response.writeHead (404).end ()
return
}
const data = responseFor (url)
response.writeHead (data == null ? 404 : 200)
response.end (JSON.stringify (data))
})
console.error ('[SSR TEST] API created')
await withTimeout ('mock API listen', new Promise ((resolve, reject) => {
api.once ('error', reject)
api.listen (0, '127.0.0.1', () => {
api.off ('error', reject)
resolve ()
})
}), 15_000)
apiURL = `http://127.0.0.1:${api.address ().port}`
console.error ('[SSR TEST] API ready:', apiURL)
node = spawn (process.execPath, ['server.mjs'], { env: {
...process.env, NODE_ENV: 'production', PORT: '0', HOST: '127.0.0.1',
PUBLIC_ORIGIN: origin, SSR_API_BASE_URL: apiURL },
stdio: ['ignore', 'pipe', 'pipe'] })
console.error ('[SSR TEST] SSR process spawned:', node.pid ?? '(no PID)')
let childOutput = ''
let childError = ''
node.stderr.on ('data', data => {
const text = data.toString ()
childError = (childError + text).slice (-8_192)
process.stderr.write (`[SSR CHILD] ${text}`)
})
httpOrigin = await withTimeout ('SSR server readiness', new Promise ((resolve, reject) => {
const onData = data => {
childOutput = (childOutput + data.toString()).slice(-8192)
// ANSI カラーコードを取り除いてから解析する
const output = childOutput.replace(/\x1b\[[0-9;]*m/g, '')
const match = output.match(/\[SSR\]\s+listening on\s+(\d+)/)
if (!match) return
console.error('[SSR TEST] detected SSR port:', match[1])
resolve(`http://127.0.0.1:${match[1]}`)
}
node.stdout.on ('data', onData)
node.once ('error', reject)
node.once ('exit', (code, signal) => reject (new Error (
`SSR process exited before readiness (code=${code}, signal=${signal})` +
`\nstdout: ${childOutput}\nstderr: ${childError}`)))
}), 30_000).catch (error => {
throw new Error (`${error.message}\nSSR stdout: ${childOutput}\nSSR stderr: ${childError}`,
{ cause: error })
})
console.error ('[SSR TEST] SSR server ready:', httpOrigin)
console.error ('[SSR TEST] starting Vite')
vite = await withTimeout ('Vite initialization',
createViteServer ({ server: { middlewareMode: true }, appType: 'custom' }), 90_000)
console.error ('[SSR TEST] Vite ready')
const { pageRoutes } = await withTimeout ('App.tsx module loading',
vite.ssrLoadModule ('/src/App.tsx'), 90_000)
console.error ('[SSR TEST] App loaded')
const flatten = (entries, parent = '') => entries.flatMap (route => {
const path = [parent, route.path].filter (Boolean).join ('/')
return route.children ? flatten (route.children, path) : [{ ...route, path }]
})
routes = flatten (pageRoutes (null, () => {}))
console.error (`[SSR TEST] setup complete (${routes.length} routes)`)
})
// Report the route on which server-side rendering stalls or fails.
const renderPage = async (url, ...args) => {
console.error (`[SSR TEST] rendering: ${url}`)
const result = await withTimeout (`render ${url}`, render (url, ...args), 60_000)
console.error (`[SSR TEST] rendered: ${url}`)
return result
}
const authenticatedCookie = async (code = recoveryCode) => {
const csrf = await fetch (`${httpOrigin}/api/users/csrf`)
assert.equal (csrf.status, 200)
const { token } = await csrf.json ()
const verified = await fetch (`${httpOrigin}/api/users/verify`, {
method: 'POST', headers: { Origin: origin, 'Content-Type': 'application/json',
Cookie: csrf.headers.get ('set-cookie').split (';')[0], 'X-CSRF-Token': token },
body: JSON.stringify ({ code, restore: true }) })
assert.equal (verified.status, 200)
assert.equal ((await verified.json ()).valid, true)
const cookie = verified.headers.get ('set-cookie')
assert.match (cookie, /HttpOnly; Secure; SameSite=Lax/)
assert.doesNotMatch (cookie, new RegExp (code))
return cookie.split (';')[0]
}
after (async () => {
console.error ('[SSR TEST] teardown started')
const errors = []
if (vite) {
try {
await withTimeout ('Vite shutdown', vite.close (), 30_000)
} catch (error) {
errors.push (error)
}
}
if (node && node.exitCode == null && node.signalCode == null) node.kill ()
if (api?.listening) {
api.closeAllConnections ()
try {
await withTimeout ('mock API shutdown', new Promise ((resolve, reject) => {
api.close (error => error ? reject (error) : resolve ())
}), 10_000)
} catch (error) {
errors.push (error)
}
}
console.error ('[SSR TEST] teardown finished')
if (errors.length) throw new AggregateError (errors, 'SSR teardown failed')
})
test ('every canonical screen route returns page HTML without JavaScript', async t => {
const cookie = await authenticatedCookie ()
for (const route of routes) {
for (const locale of ['ja', 'ain-Kana']) {
const path = route.path.replace (':identifier', 'home').replace (/:id|:title/g, '1')
.replace ('*', 'unknown-screen')
const url = `/${locale}/${path}?from=2026-09-15&to=2026-09-15`
await t.test (`${locale} ${route.path}`, async () => {
const result = await renderPage (url, origin, apiURL, { cookie })
assert.ok (result != null)
if (route.handle?.redirectTo) {
assert.equal (result.status, 308)
return
}
assert.ok (result.html?.length > 300, 'actual page HTML is required')
assert.equal (result.status, route.path === '*' ? 404 : 200)
assert.match (result.head, /<title/)
assert.match (result.head, /rel="canonical"/)
assert.equal (result.locale, locale)
assert.doesNotMatch (result.html + result.bootstrap, /test-recovery-secret|opaque-rails/)
if (route.path === 'wiki/:id/edit') assert.match (result.html, /<textarea/)
if (route.path === 'users/settings') assert.match (result.html, /SSR account/)
if (route.path === 'posts')
assert.match (result.html, new RegExp (`SSR post ${locale}`))
if (route.path === 'posts/:id')
assert.match (result.head, new RegExp (`SSR post ${locale}`))
if (route.path === 'theatres/:id')
assert.match (result.html.replace (/<[^>]*>/g, ''), /SSR[  ]theatre/)
if (route.path === 'wiki' || route.path === 'wiki/:title')
assert.match (result.html, new RegExp (`SSR wiki body ${locale}|SSR`))
assert.doesNotMatch (result.html, /Switched to client rendering|template id="B:/)
})
}
}
})
test ('YouTube posts return HTTP 200 with the embed rendered by SSR', async t => {
for (const locale of ['ja', 'ain-Kana']) {
await t.test (locale, async () => {
const response = await fetch (`${httpOrigin}/${locale}/posts/2`)
assert.equal (response.status, 200)
const html = await response.text ()
assert.match (html, new RegExp (`<html lang="${locale}"`))
assert.match (html, new RegExp (`SSR post ${locale}`))
assert.match (html, /https:\/\/www\.youtube\.com\/watch\?v=dQw4w9WgXcQ/)
// react-youtube renders its player container before mounting the iframe.
assert.match (html, /<div class=""><div id="" class=""><\/div><\/div>/)
assert.doesNotMatch (html, /Switched to client rendering|template id="B:/)
})
}
})
test ('parallel localized requests keep list data isolated', async () => {
const start = requests.length
const pages = await Promise.all (['ja', 'ain-Kana', 'ja', 'ain-Kana'].map (locale =>
renderPage (`/${locale}/posts?limit=50&page=2`, origin, apiURL)))
for (const [index, page] of pages.entries ()) {
const locale = index % 2 === 0 ? 'ja' : 'ain-Kana'
assert.match (page.html, new RegExp (`SSR post ${locale}`))
assert.equal (page.status, 200)
const state = JSON.parse (page.bootstrap)
assert.equal (state.locale, locale)
}
assert.ok (requests.some (request => request.url.includes ('page=2')))
assert.ok (requests.slice (start).every (request => request.method === 'GET'))
})
test ('unknown resources and legacy URLs have correct status', async () => {
assert.equal ((await renderPage ('/ja/unknown', origin, apiURL)).status, 404)
assert.equal ((await renderPage ('/ja/help/unknown', origin, apiURL)).status, 404)
assert.equal ((await renderPage ('/ja/posts/999', origin, apiURL)).status, 404)
const redirect = await renderPage ('/help/home?edition=2026-09-15', origin, apiURL)
assert.equal (redirect.status, 308)
assert.equal (redirect.location, '/ja/help/home?edition=2026-09-15')
})
test ('HTTP forwards Rails cookies, CSRF, expiration and upstream errors', async () => {
const cookie = await authenticatedCookie ()
const settings = await fetch (`${httpOrigin}/ja/users/settings`, { headers: { Cookie: cookie } })
assert.equal (settings.status, 200)
const html = await settings.text ()
assert.match (html, /SSR account/)
assert.doesNotMatch (html, /test-recovery-secret|opaque-rails-session/)
assert.match (settings.headers.get ('cache-control'), /private, no-store/)
assert.match (settings.headers.get ('set-cookie'), /Max-Age=630720000/)
assert.ok (requests.some (request => request.url === '/users/session'
&& request.cookie === cookie))
assert.ok (requests.every (request => request.transferCode == null))
assert.equal ((await fetch (`${httpOrigin}/ja/users/settings`)).status, 401)
const invalid = cookie.replace ('=', '=invalid')
assert.equal ((await fetch (`${httpOrigin}/ja/users/settings`, {
headers: { Cookie: invalid } })).status, 401)
const publicPage = await fetch (`${httpOrigin}/ain-Kana/posts`)
assert.equal (publicPage.status, 200)
assert.match (await publicPage.text (), /SSR post ain-Kana/)
assert.equal ((await fetch (`${httpOrigin}/_ssr/session`, { method: 'POST' })).status, 405)
const csrf = await fetch (`${httpOrigin}/api/users/csrf`, { headers: { Cookie: cookie } })
const { token } = await csrf.json ()
const write = (csrfToken, requestOrigin = origin) => fetch (
`${httpOrigin}/api/users/verify`, { method: 'POST',
headers: { Cookie: cookie, Origin: requestOrigin, 'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken }, body: JSON.stringify ({ code: recoveryCode }) })
assert.equal ((await write ('invalid')).status, 403)
assert.equal ((await write (token, 'https://other.invalid')).status, 403)
assert.equal ((await fetch (`${httpOrigin}/ja/users/settings`, {
headers: { Cookie: cookie } })).status, 200)
const renewed = await write (token)
assert.equal (renewed.status, 200)
const renewedCookie = renewed.headers.get ('set-cookie').split (';')[0]
assert.notEqual (renewedCookie, cookie)
assert.equal ((await fetch (`${httpOrigin}/ja/users/settings`, {
headers: { Cookie: cookie } })).status, 401)
const renewedCSRF = await fetch (`${httpOrigin}/api/users/csrf`, {
headers: { Cookie: renewedCookie } })
const newToken = (await renewedCSRF.json ()).token
const logout = await fetch (`${httpOrigin}/api/users/session`, { method: 'DELETE',
headers: { Cookie: renewedCookie, Origin: origin, 'X-CSRF-Token': newToken } })
assert.equal (logout.status, 204)
assert.match (logout.headers.get ('set-cookie'), /Max-Age=0/)
assert.equal ((await fetch (`${httpOrigin}/ja/users/settings`, {
headers: { Cookie: renewedCookie } })).status, 401)
failAPI = true
try {
assert.equal ((await fetch (`${httpOrigin}/ja/posts`)).status, 503)
} finally {
failAPI = false
}
})
test ('parallel user and Locale requests never share authentication or query state', async () => {
const cookies = await Promise.all ([authenticatedCookie (),
authenticatedCookie ('second-recovery-secret')])
const inputs = cookies.flatMap ((cookie, user) => ['ja', 'ain-Kana'].map (locale => ({
cookie, locale, name: user === 0 ? 'SSR account' : 'Second account',
other: user === 0 ? 'Second account' : 'SSR account' })))
const pages = await Promise.all (inputs.map (async input => {
const response = await fetch (`${httpOrigin}/${input.locale}/users/settings`, {
headers: { Cookie: input.cookie } })
assert.equal (response.status, 200)
return response.text ()
}))
for (const [index, html] of pages.entries ()) {
const input = inputs[index]
assert.match (html, new RegExp (input.name))
assert.doesNotMatch (html, new RegExp (input.other))
assert.match (html, new RegExp (`<html lang="${input.locale}"`))
assert.doesNotMatch (html, /recovery-secret|opaque-rails-session|csrf-/)
}
})
test ('renewed recovery codes invalidate old sessions and forward the new Rails cookie',
async () => {
const cookie = await authenticatedCookie ('second-recovery-secret')
const csrf = await fetch (`${httpOrigin}/api/users/csrf`, { headers: { Cookie: cookie } })
const { token } = await csrf.json ()
const renewed = await fetch (`${httpOrigin}/api/users/code/renew`, {
method: 'POST', headers: { Cookie: cookie, Origin: origin, 'X-CSRF-Token': token } })
assert.equal (renewed.status, 200)
const newCookie = renewed.headers.get ('set-cookie').split (';')[0]
assert.notEqual (newCookie, cookie)
assert.equal ((await fetch (`${httpOrigin}/ja/users/settings`, {
headers: { Cookie: cookie } })).status, 401)
assert.equal ((await fetch (`${httpOrigin}/ain-Kana/users/settings`, {
headers: { Cookie: newCookie } })).status, 200)
})
test ('hydrate every route with stored browser preferences', {
skip: !process.env.SSR_CHROME_PATH, timeout: 180_000,
}, async t => {
const { default: WebSocket } = await import ('ws')
const chrome = spawn (process.env.SSR_CHROME_PATH, [
'--headless', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage',
'--remote-debugging-port=0', `--user-data-dir=/tmp/btrc-ssr-browser-${process.pid}`],
{ stdio: ['ignore', 'ignore', 'pipe'] })
let ws
try {
const endpoint = await new Promise ((resolve, reject) => {
let output = ''
chrome.stderr.on ('data', data => {
output += data
const match = output.match (/DevTools listening on (ws:\/\/\S+)/)
if (match) resolve (match[1])
})
chrome.on ('error', reject)
})
ws = new WebSocket (endpoint)
await new Promise (resolve => ws.on ('open', resolve))
const pending = new Map ()
let sequence = 0, errors = [], probeActive = false, delayedChunks = 0, documents = 0
const send = (method, params = {}, sessionId) => new Promise ((resolve, reject) => {
const id = ++sequence
pending.set (id, { resolve, reject })
ws.send (JSON.stringify ({ id, method, params, sessionId }))
})
ws.on ('message', raw => {
const message = JSON.parse (raw)
if (message.id) {
const entry = pending.get (message.id)
pending.delete (message.id)
if (message.error) entry?.reject (message.error)
else entry?.resolve (message.result)
} else if (message.method === 'Fetch.requestPaused') {
const { requestId, request } = message.params
const url = new URL (request.url)
if (url.origin === httpOrigin) {
const delayed = probeActive
&& /\/(MaterialBasePage|MaterialListPage|TagListPage)-[^/]+\.js$/.test (url.pathname)
if (delayed) ++delayedChunks
setTimeout (() => {
void send ('Fetch.continueRequest', { requestId }, message.sessionId)
}, delayed ? 600 : 0)
return
}
const data = responseFor (url)
void send ('Fetch.fulfillRequest', { requestId,
responseCode: request.method === 'OPTIONS' ? 204 : 200,
responseHeaders: [{ name: 'Content-Type', value: 'application/json' },
{ name: 'Access-Control-Allow-Origin', value: httpOrigin },
{ name: 'Access-Control-Allow-Headers', value: '*' },
{ name: 'Access-Control-Allow-Methods', value: '*' }],
body: Buffer.from (JSON.stringify (data ?? {})).toString ('base64') }, message.sessionId)
} else if (message.method === 'Runtime.exceptionThrown') {
errors.push (message.params.exceptionDetails.exception?.description
?? message.params.exceptionDetails.text)
} else if (message.method === 'Runtime.consoleAPICalled'
&& message.params.type === 'error') {
errors.push (message.params.args.map (arg => arg.value ?? arg.description).join (' '))
} else if (message.method === 'Network.requestWillBeSent'
&& message.params.type === 'Document') {
++documents
}
})
const { targetId } = await send ('Target.createTarget', { url: 'about:blank' })
const { sessionId } = await send ('Target.attachToTarget', { targetId, flatten: true })
await send ('Runtime.enable', {}, sessionId)
await send ('Page.enable', {}, sessionId)
await send ('Network.enable', {}, sessionId)
await send ('Network.setCacheDisabled', { cacheDisabled: true }, sessionId)
await send ('Fetch.enable', { patterns: [{ urlPattern: '*' }] }, sessionId)
const cookie = await authenticatedCookie ()
await send ('Network.setExtraHTTPHeaders', { headers: {
Cookie: cookie } }, sessionId)
await send ('Page.addScriptToEvaluateOnNewDocument', { source: `
localStorage.setItem('user_code','test-recovery-secret');
localStorage.setItem('btrc_hub.client_settings',JSON.stringify({
lists:{postList:{limit:50},tagList:{limit:50},postSearch:{limit:50}},
behavior:{animation:new URLSearchParams(location.search).get('animation')??'off'},
theatre:{layoutMode:'tagsBottom',tagFlow:'horizontal'}
}));
new MutationObserver(()=>{
const child=document.getElementById('root')?.firstElementChild;
if(child&&!window.__initialSSRNode)window.__initialSSRNode=child;
}).observe(document,{childList:true,subtree:true});
if(new URLSearchParams(location.search).has('layoutProbe')){
window.__layoutProbe={samples:[],mainLost:false,earlyAnimation:false};
let originalMain=null;
const sample=()=>{
const main=document.querySelector('main');
const layout=document.querySelector('.top-nav-layout');
const controls=document.querySelector('.top-nav-mobile-controls');
if(main&&layout&&controls){
originalMain??=main;
if(!originalMain.isConnected)window.__layoutProbe.mainLost=true;
const ready=layout.dataset.layoutReady==='true';
const state={mobile:getComputedStyle(controls).display!=='none',
measured:layout.hasAttribute('data-desktop'),ready};
const previous=window.__layoutProbe.samples.at(-1);
if(JSON.stringify(previous)!==JSON.stringify(state))
window.__layoutProbe.samples.push(state);
if(!ready&&layout.getAnimations({subtree:true})
.some(animation=>animation.playState==='running'))
window.__layoutProbe.earlyAnimation=true;
}else if(originalMain&&!originalMain.isConnected){
window.__layoutProbe.mainLost=true;
}
requestAnimationFrame(sample);
};
requestAnimationFrame(sample);
}` }, sessionId)
const evaluate = async expression => (await send ('Runtime.evaluate', {
expression, returnByValue: true }, sessionId)).result.value
for (const route of routes.filter (entry => !entry.handle?.redirectTo)) {
for (const locale of ['ja', 'ain-Kana']) {
await t.test (`hydrate ${locale} ${route.path}`, async () => {
const path = route.path.replace (':identifier', 'home').replace (/:id|:title/g, '1')
.replace ('*', 'unknown-screen')
errors = []
await send ('Page.navigate', {
url: `${httpOrigin}/${locale}/${path}?from=2026-09-15&to=2026-09-15` }, sessionId)
let hydrated = false
for (let attempt = 0; attempt < 100; ++attempt) {
await new Promise (resolve => setTimeout (resolve, 75))
try {
hydrated = await evaluate (`Object.keys(document.getElementById('root')??{})
.some(key=>key.startsWith('__reactContainer'))`)
} catch {}
if (hydrated) break
}
assert.ok (hydrated, 'hydrateRoot must run')
await new Promise (resolve => setTimeout (resolve, 150))
assert.deepEqual (errors, [])
assert.ok (await evaluate (`document.getElementById('root').firstElementChild
===window.__initialSSRNode`), 'server DOM must be preserved')
assert.equal (await evaluate ('document.documentElement.lang'), locale)
})
}
}
probeActive = true
for (const width of [375, 1240]) {
await send ('Emulation.setDeviceMetricsOverride', {
width, height: 900, deviceScaleFactor: 1, mobile: width < 768 }, sessionId)
for (const animation of ['off', 'reduced', 'normal']) {
for (const locale of ['ja', 'ain-Kana']) {
for (const page of ['materials', 'tags']) {
await t.test (`stable first paint ${width} ${animation} ${locale} ${page}`,
async () => {
errors = []
documents = 0
delayedChunks = 0
await send ('Page.navigate', { url:
`${httpOrigin}/${locale}/${page}?layoutProbe=1&animation=${animation}` }, sessionId)
let ready = false
for (let attempt = 0; attempt < 100; ++attempt) {
await new Promise (resolve => setTimeout (resolve, 75))
try {
ready = await evaluate (`document.querySelector('.top-nav-layout')
?.dataset.layoutReady==='true'
&&Object.keys(document.getElementById('root')??{})
.some(key=>key.startsWith('__reactContainer'))`)
} catch {}
if (ready) break
}
assert.ok (ready, 'hydration and the first layout measurement must finish')
await new Promise (resolve => setTimeout (resolve, 150))
const probe = await evaluate ('window.__layoutProbe')
assert.ok (delayedChunks >= (page === 'materials' ? 2 : 1))
assert.equal (documents, 1)
assert.deepEqual (errors, [])
assert.equal (probe.mainLost, false, 'SSR page content must stay attached')
assert.equal (probe.earlyAnimation, false, 'first layout must not animate')
const initial = probe.samples.find (sample => !sample.measured)
assert.ok (initial, 'observe the CSS layout before hydration')
assert.equal (initial.mobile, width < 768)
const final = probe.samples.at (-1)
if (!final.mobile)
assert.ok (probe.samples.every (sample => !sample.mobile),
'a desktop page must not initially show the mobile controls')
assert.ok (await evaluate (`document.documentElement.scrollWidth<=innerWidth`),
'the measured layout must fit the viewport')
})
}
}
}
}
} finally {
ws?.close ()
chrome.kill ()
}
})