@@ -47,6 +47,35 @@ const PER_WORKSPACE_READ = [
4747]
4848const isPerWorkspaceRead = ( path : string ) => PER_WORKSPACE_READ . some ( ( read ) => read . test ( path ) )
4949
50+ type FaviconTrace = Window & { faviconTrace ?: string [ ] }
51+
52+ /**
53+ * Runs in the page: logs each favicon `<img>` the document gains or loses. React's streamed reveal
54+ * moves one (it stays connected), and a discarded boundary removes it for good.
55+ */
56+ function traceFaviconImages ( ) {
57+ const trace : string [ ] = [ ]
58+ ; ( window as FaviconTrace ) . faviconTrace = trace
59+ const faviconsIn = ( node : Node ) =>
60+ node instanceof Element
61+ ? [ node , ...node . querySelectorAll ( 'img' ) ] . filter (
62+ ( element ) => element . tagName === 'IMG' && element . getAttribute ( 'src' ) ?. includes ( 'favicon' )
63+ )
64+ : [ ]
65+ const log = ( change : string , nodes : NodeList ) => {
66+ const stamp = `${ Math . round ( performance . now ( ) ) } ms`
67+ for ( const node of nodes )
68+ for ( const image of faviconsIn ( node ) )
69+ trace . push ( `${ stamp } ${ change } ${ image . getAttribute ( 'src' ) } connected=${ image . isConnected } ` )
70+ }
71+ new MutationObserver ( ( mutations ) => {
72+ for ( const { addedNodes, removedNodes } of mutations ) {
73+ log ( 'added' , addedNodes )
74+ log ( 'removed' , removedNodes )
75+ }
76+ } ) . observe ( document , { childList : true , subtree : true } )
77+ }
78+
5079export async function firstPaintChecks ( ) {
5180 section ( '6 First paint' )
5281 const pages = [
@@ -180,11 +209,21 @@ export async function firstPaintChecks() {
180209 async ( ) => {
181210 await withSession ( persona , async ( session ) => {
182211 const { page } = session
183- // A returning member's browser already holds the app's scripts.
212+ // `open` routes the socket token, which turns off the context's HTTP cache, so both loads
213+ // fetch every script: hydration can wait on a route's chunk, as on a first visit.
184214 await page . goto ( orgUrl ( `/${ entry . path } ` ) )
185215 await entry . primary ( page ) . first ( ) . waitFor ( { timeout : 30_000 } )
186216 await session . settle ( )
187217 session . requests . length = 0
218+ const hydrationErrors : string [ ] = [ ]
219+ page . on ( 'pageerror' , ( { message } ) => {
220+ if ( / h y d r a t | # 4 ( 1 8 | 1 9 | 2 3 | 2 5 ) \b / i. test ( message ) ) hydrationErrors . push ( message )
221+ } )
222+ page . on ( 'console' , ( message ) => {
223+ if ( / h y d r a t | # 4 ( 1 8 | 1 9 | 2 3 | 2 5 ) \b / i. test ( message . text ( ) ) )
224+ hydrationErrors . push ( message . text ( ) )
225+ } )
226+ await page . addInitScript ( traceFaviconImages )
188227 const started = performance . now ( )
189228 await page . goto ( orgUrl ( `/${ entry . path } ` ) , { waitUntil : 'commit' } )
190229 await entry . primary ( page ) . first ( ) . waitFor ( { timeout : 30_000 } )
@@ -222,7 +261,18 @@ export async function firstPaintChecks() {
222261 prefetchedAndRefetched : refetched ,
223262 perWorkspaceReads,
224263 } )
225- assert . deepEqual ( duplicates , [ ] , 'A read was fetched twice on first paint' )
264+ const faviconTrace = await page . evaluate (
265+ ( ) => ( window as FaviconTrace ) . faviconTrace ?? [ ]
266+ )
267+ assert . deepEqual (
268+ duplicates ,
269+ [ ] ,
270+ [
271+ 'A read was fetched twice on first paint' ,
272+ `Hydration errors: ${ JSON . stringify ( hydrationErrors ) } ` ,
273+ `Favicon images: ${ JSON . stringify ( faviconTrace ) } ` ,
274+ ] . join ( '\n' )
275+ )
226276 assert . deepEqual ( refetched , [ ] , 'A prefetched key was fetched again on first paint' )
227277 assert . deepEqual ( perWorkspaceReads , [ ] , 'The composer read every workspace on load' )
228278 } )
0 commit comments