このコミットが含まれているのは:
2026-10-20 02:42:14 +09:00
コミット e8d4c5ad09
7個のファイルの変更、256行の追加、86行の削除
+87 -4
ファイルの表示
@@ -470,7 +470,7 @@ test ('hydrate every route with stored browser preferences', {
ws = new WebSocket (endpoint)
await new Promise (resolve => ws.on ('open', resolve))
const pending = new Map ()
let sequence = 0, errors = []
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 })
@@ -487,7 +487,12 @@ test ('hydrate every route with stored browser preferences', {
const { requestId, request } = message.params
const url = new URL (request.url)
if (url.origin === httpOrigin) {
void send ('Fetch.continueRequest', { requestId }, message.sessionId)
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)
@@ -504,6 +509,9 @@ test ('hydrate every route with stored browser preferences', {
} 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' })
@@ -511,6 +519,7 @@ test ('hydrate every route with stored browser preferences', {
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: {
@@ -519,12 +528,39 @@ test ('hydrate every route with stored browser preferences', {
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:'off'},theatre:{layoutMode:'tagsBottom',tagFlow:'horizontal'}
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});` }, sessionId)
}).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)) {
@@ -553,6 +589,53 @@ test ('hydrate every route with stored browser preferences', {
})
}
}
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 ()