このコミットが含まれているのは:
@@ -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 ()
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする