1- import { test , expect } from '@playwright/test' ;
2- import { HERO_ROUTES } from '../src/components/shared/nav-config' ;
1+ import { test , expect , type Page } from '@playwright/test' ;
32
43/**
5- * A hand-maintained hero-route list drifts. A unit test over the list cannot
6- * catch a page that stopped rendering a hero, so the guard has to visit the
7- * page and read the computed background.
4+ * The bar is one CSS surface: a translucent white over a backdrop blur, on
5+ * every route at every scroll position. There is no route list, no sentinel,
6+ * no observer and no `data-surface` attribute any more, so there is nothing
7+ * here to assert about state — only that the single surface is actually the
8+ * one that renders.
89 *
9- * The background is asserted with `toHaveCSS` rather than a one-shot
10- * `getComputedStyle` read: `.nav-bar` transitions `background` over 200ms, so
11- * the attribute flips a fifth of a second before the colour finishes moving,
12- * and a single read lands mid-fade on a partial alpha. `toHaveCSS` retries,
13- * which is what makes this assert the resting surface instead of the timing.
10+ * Two things can silently take that away, which is why this suite reads the
11+ * computed style out of a real browser rather than trusting the source:
12+ *
13+ * 1. The blur is prefixed by Lightning CSS, not by hand. Writing
14+ * `-webkit-backdrop-filter` in chrome.css makes Lightning collapse the pair
15+ * down to the prefixed property alone, and Chromium does not implement
16+ * `-webkit-backdrop-filter` at all — the bar keeps its 72% alpha and loses
17+ * the blur, which is an unreadable smear rather than a visible failure.
18+ * 2. Anything that reintroduces a scroll- or route-dependent surface brings
19+ * back the hydration flash this replaced.
1420 */
15- for ( const route of HERO_ROUTES ) {
16- test ( `the nav is transparent at rest on ${ route } ` , async ( { page } ) => {
17- await page . setViewportSize ( { width : 1440 , height : 900 } ) ;
18- await page . goto ( route ) ;
19- const nav = page . locator ( 'nav' ) . first ( ) ;
20- await expect ( nav ) . toHaveAttribute ( 'data-surface' , 'transparent' ) ;
21- await expect ( nav ) . toHaveCSS ( 'background-color' , 'rgba(0, 0, 0, 0)' ) ;
21+
22+ const DOCS_ROUTE = '/docs/langgraph/getting-started/introduction' ;
23+
24+ interface BarSurface {
25+ readonly background : string ;
26+ readonly backdropFilter : string ;
27+ readonly boxShadow : string ;
28+ readonly borderBottomColor : string ;
29+ }
30+
31+ async function readBarSurface ( page : Page ) : Promise < BarSurface > {
32+ return page . evaluate ( ( ) => {
33+ const bar = document . querySelector ( '.nav-bar' ) ;
34+ if ( ! bar ) throw new Error ( 'no .nav-bar on the page' ) ;
35+ const style = getComputedStyle ( bar ) ;
36+ return {
37+ background : style . backgroundColor ,
38+ backdropFilter : style . backdropFilter ,
39+ boxShadow : style . boxShadow ,
40+ borderBottomColor : style . borderBottomColor ,
41+ } ;
2242 } ) ;
43+ }
44+
45+ /** The alpha of an `rgb()`/`rgba()` computed colour; 1 when none is present. */
46+ function alphaOf ( color : string ) : number {
47+ const parts = color . match ( / - ? [ \d . ] + / g) ;
48+ if ( ! parts ) throw new Error ( `unparseable colour: ${ color } ` ) ;
49+ return parts . length >= 4 ? Number ( parts [ 3 ] ) : 1 ;
50+ }
2351
24- test ( `the nav solidifies once ${ route } is scrolled` , async ( { page } ) => {
52+ for ( const [ label , route ] of [
53+ [ 'the marketing hero' , '/' ] ,
54+ [ 'a docs page' , DOCS_ROUTE ] ,
55+ ] as const ) {
56+ test ( `the nav bar is translucent and blurred on ${ label } ` , async ( {
57+ page,
58+ } ) => {
2559 await page . setViewportSize ( { width : 1440 , height : 900 } ) ;
2660 await page . goto ( route ) ;
27- await page . mouse . wheel ( 0 , 600 ) ;
61+ await expect ( page . locator ( 'nav' ) . first ( ) ) . toBeVisible ( ) ;
62+
63+ const surface = await readBarSurface ( page ) ;
64+
65+ // Strictly between 0 and 1: fully opaque is the old solid bar, fully
66+ // transparent is the old hero state. Neither exists any more.
67+ const alpha = alphaOf ( surface . background ) ;
68+ expect ( alpha , `background was ${ surface . background } ` ) . toBeGreaterThan ( 0 ) ;
69+ expect ( alpha , `background was ${ surface . background } ` ) . toBeLessThan ( 1 ) ;
2870
29- const nav = page . locator ( 'nav' ) . first ( ) ;
30- await expect ( nav ) . toHaveAttribute ( 'data-surface' , 'solid' ) ;
31- await expect ( nav ) . not . toHaveCSS ( 'background-color' , 'rgba(0, 0, 0, 0)' ) ;
71+ // `none` here is the Lightning-CSS prefix trap in the header comment: the
72+ // translucency survives it, so only this read catches it.
73+ expect ( surface . backdropFilter ) . not . toBe ( 'none' ) ;
74+ expect ( surface . backdropFilter ) . toContain ( 'blur' ) ;
75+
76+ // The redesign removed the shadow deliberately; the hairline is the edge.
77+ expect ( surface . boxShadow ) . toBe ( 'none' ) ;
78+ expect ( surface . borderBottomColor ) . not . toBe ( 'rgba(0, 0, 0, 0)' ) ;
3279 } ) ;
3380}
3481
35- test ( 'the nav is solid on a route with no hero' , async ( { page } ) => {
82+ /**
83+ * The guard for the whole simplification. `useNavSurface`, its 8px sentinel and
84+ * its IntersectionObserver existed only to change this value on scroll; if any
85+ * of that comes back — or a scroll listener, or a route-conditional class —
86+ * these two reads stop matching.
87+ *
88+ * Proved non-vacuous by mutation: adding a rule that repaints `.nav-bar` once
89+ * the page is scrolled fails this case on the background line.
90+ */
91+ test ( 'the nav bar surface does not change when the page is scrolled' , async ( {
92+ page,
93+ } ) => {
3694 await page . setViewportSize ( { width : 1440 , height : 900 } ) ;
37- await page . goto ( '/docs/langgraph/getting-started/introduction' ) ;
38- await expect ( page . locator ( 'nav' ) . first ( ) ) . toHaveAttribute (
39- 'data-surface' ,
40- 'solid' ,
41- ) ;
95+ await page . goto ( '/' ) ;
96+ await expect ( page . locator ( 'nav' ) . first ( ) ) . toBeVisible ( ) ;
97+
98+ const atTop = await readBarSurface ( page ) ;
99+
100+ await page . mouse . wheel ( 0 , 900 ) ;
101+ await page . waitForFunction ( ( ) => window . scrollY > 400 ) ;
102+ // Long enough that a reintroduced 200ms surface transition would have
103+ // finished, so a difference here is a real difference and not a fade caught
104+ // mid-flight.
105+ await page . waitForTimeout ( 600 ) ;
106+ const scrolled = await readBarSurface ( page ) ;
107+
108+ expect ( scrolled ) . toEqual ( atTop ) ;
42109} ) ;
43110
44111/**
@@ -48,11 +115,9 @@ test('the nav is solid on a route with no hero', async ({ page }) => {
48115 * also result from a filled button that happened to be 25px tall, so nothing
49116 * else asserts the surface actually changed.
50117 *
51- * Marketing is asserted as "has a fill", not "has a yellow fill": at rest
52- * (scroll 0) `/` is a HERO_ROUTES page with a transparent `.nav-bar`, and the
53- * transparent-surface rule inverts the CTA to a navy fill rather than leaving
54- * it in its normal yellow — asserting a specific colour here would encode
55- * that scroll-position inversion and break the moment either theme changes.
118+ * Marketing is asserted as "has a fill", not "has a yellow fill", so the case
119+ * survives a retheme; the docs side can name its colour because the demotion is
120+ * specifically to `--color-accent` as a text link.
56121 *
57122 * Both reads use `toHaveCSS`, not a one-shot `getComputedStyle`: the button
58123 * itself transitions `background-color`/`color` over 120ms on mount, so an
@@ -73,7 +138,7 @@ test('the nav CTA is a filled button on marketing but a text link on docs', asyn
73138 'rgba(0, 0, 0, 0)' ,
74139 ) ;
75140
76- await page . goto ( '/docs/langgraph/getting-started/introduction' ) ;
141+ await page . goto ( DOCS_ROUTE ) ;
77142 const docsCta = page
78143 . locator ( 'nav' )
79144 . first ( )
0 commit comments