@@ -461,6 +461,88 @@ test(
461461 { onlyLibs : [ 'data-client' ] } ,
462462) ;
463463
464+ // ── TIMING VALIDATION ────────────────────────────────────────────────
465+ // Verify that when data-bench-complete fires (measurement ends), the DOM
466+ // already reflects the update. A 100ms network delay makes timing bugs
467+ // observable: if the measureUpdate callback doesn't return its promise
468+ // chain, finish() fires via the sync-path double-rAF (~32ms) before the
469+ // async fetch resolves. data-client passes because controller.fetch()
470+ // dispatches optimistic updates to the store synchronously.
471+
472+ test ( 'updateEntity timing: DOM reflects change at measurement end' , async ( page , lib ) => {
473+ await initAndWaitForItems ( page ) ;
474+ await page . evaluate ( ( ) => window . __BENCH__ ! . setNetworkDelay ( 100 ) ) ;
475+
476+ await clearComplete ( page ) ;
477+ await page . evaluate ( ( ) => window . __BENCH__ ! . updateEntity ( 'item-0' ) ) ;
478+ await waitForComplete ( page ) ;
479+
480+ const labels = await getItemLabels ( page ) ;
481+ assert (
482+ labels [ 'item-0' ] ?. includes ( '(updated)' ) ?? false ,
483+ lib ,
484+ 'updateEntity timing' ,
485+ `DOM not updated when data-bench-complete fired. ` +
486+ `Ensure measureUpdate callback returns its promise chain.` ,
487+ ) ;
488+
489+ await page . evaluate ( ( ) => window . __BENCH__ ! . setNetworkDelay ( 0 ) ) ;
490+ } ) ;
491+
492+ test ( 'createEntity timing: DOM reflects change at measurement end' , async ( page , lib ) => {
493+ if (
494+ ! ( await page . evaluate (
495+ ( ) => typeof window . __BENCH__ ?. createEntity === 'function' ,
496+ ) )
497+ )
498+ return ;
499+
500+ await initAndWaitForItems ( page , 10 ) ;
501+ await page . evaluate ( ( ) => window . __BENCH__ ! . setNetworkDelay ( 100 ) ) ;
502+
503+ await clearComplete ( page ) ;
504+ await page . evaluate ( ( ) => window . __BENCH__ ! . createEntity ! ( ) ) ;
505+ await waitForComplete ( page ) ;
506+
507+ const labels = await getItemLabels ( page ) ;
508+ assert (
509+ Object . values ( labels ) . some ( l => l === 'New Item' ) ,
510+ lib ,
511+ 'createEntity timing' ,
512+ `"New Item" not in DOM when data-bench-complete fired. ` +
513+ `Ensure measureUpdate callback returns its promise chain.` ,
514+ ) ;
515+
516+ await page . evaluate ( ( ) => window . __BENCH__ ! . setNetworkDelay ( 0 ) ) ;
517+ } ) ;
518+
519+ test ( 'deleteEntity timing: DOM reflects change at measurement end' , async ( page , lib ) => {
520+ if (
521+ ! ( await page . evaluate (
522+ ( ) => typeof window . __BENCH__ ?. deleteEntity === 'function' ,
523+ ) )
524+ )
525+ return ;
526+
527+ await initAndWaitForItems ( page , 10 ) ;
528+ await page . evaluate ( ( ) => window . __BENCH__ ! . setNetworkDelay ( 100 ) ) ;
529+
530+ await clearComplete ( page ) ;
531+ await page . evaluate ( ( ) => window . __BENCH__ ! . deleteEntity ! ( 'item-0' ) ) ;
532+ await waitForComplete ( page ) ;
533+
534+ const labels = await getItemLabels ( page ) ;
535+ assert (
536+ ! ( 'item-0' in labels ) ,
537+ lib ,
538+ 'deleteEntity timing' ,
539+ `item-0 still in DOM when data-bench-complete fired. ` +
540+ `Ensure measureUpdate callback returns its promise chain.` ,
541+ ) ;
542+
543+ await page . evaluate ( ( ) => window . __BENCH__ ! . setNetworkDelay ( 0 ) ) ;
544+ } ) ;
545+
464546// ═══════════════════════════════════════════════════════════════════════════
465547// Runner
466548// ═══════════════════════════════════════════════════════════════════════════
0 commit comments