@@ -50,4 +50,81 @@ test.describe('nested-rsc-css-hmr', () => {
5050 'rgb(255, 165, 0)' ,
5151 )
5252 } )
53+
54+ // Verifies that *removing* a CSS rule (not just changing its value) takes
55+ // effect across HMR edits. Because plugin-rsc's HMR fix appends a
56+ // `?t=<ts>` cache-buster to the emitted `<link href>` on every RSC
57+ // re-render, each edit produces a `<link>` with a new href. This test
58+ // asserts two things that a value-only edit can't catch:
59+ //
60+ // 1. The previous `<link>` is actually unmounted by React on each edit
61+ // (no DOM accumulation). If old `<link>` nodes lingered — as happens
62+ // when React 19 Float manages them by precedence and dedupes by href
63+ // rather than by intent — a deleted property would still cascade from
64+ // the stale stylesheet and the change would be silently lost.
65+ // 2. Commenting out the `color` rule actually falls back to the UA
66+ // default (`rgb(0, 0, 0)`), which exercises the unmount path end to
67+ // end (browser drops the old sheet, no rule applies anymore).
68+ //
69+ // The link-count assertion uses `[data-rsc-css-href]` so it only counts
70+ // RSC-emitted stylesheets for `inner.css` and ignores other links Vite
71+ // or React may inject (HMR client style tags, preload hints, etc).
72+ //
73+ // Note: in this fixture two `<link>`s for `inner.css` exist on initial
74+ // load — one from the outer Root tree's `collectCss`, one from the
75+ // nested Flight stream's own `collectCss`. The accumulation bug we want
76+ // to catch is "count grows per edit" (yak-style), so we capture the
77+ // initial count and assert it stays equal across edits — not that it
78+ // equals 1.
79+ test ( 'round-trip with property removal does not leave stale link' , async ( {
80+ page,
81+ } ) => {
82+ await page . goto ( f . url ( ) )
83+ await waitForHydration ( page )
84+ await expect ( page . locator ( '.test-nested-rsc-inner' ) ) . toHaveCSS (
85+ 'color' ,
86+ 'rgb(255, 165, 0)' ,
87+ )
88+
89+ const innerLinks = page . locator (
90+ 'link[rel="stylesheet"][data-rsc-css-href*="inner.css"]' ,
91+ )
92+ const initialLinkCount = await innerLinks . count ( )
93+ expect ( initialLinkCount ) . toBeGreaterThan ( 0 )
94+
95+ await using _ = await expectNoReload ( page )
96+ const editor = f . createEditor ( 'src/nested-rsc/inner.css' )
97+
98+ // Edit 1: change value
99+ editor . edit ( ( s ) => s . replaceAll ( 'rgb(255, 165, 0)' , 'rgb(0, 165, 255)' ) )
100+ await expect ( page . locator ( '.test-nested-rsc-inner' ) ) . toHaveCSS (
101+ 'color' ,
102+ 'rgb(0, 165, 255)' ,
103+ )
104+ await expect ( innerLinks ) . toHaveCount ( initialLinkCount )
105+
106+ // Edit 2: remove the rule — color must fall back to the inherited
107+ // value (`:root { color: #213547 }` from `index.css`, light scheme =
108+ // rgb(33, 53, 71)). If any old `<link>` for `inner.css` were still
109+ // attached, the cascade would keep the blue.
110+ editor . edit ( ( s ) =>
111+ s . replaceAll (
112+ 'color: rgb(0, 165, 255);' ,
113+ '/* color: rgb(0, 165, 255); */' ,
114+ ) ,
115+ )
116+ await expect ( page . locator ( '.test-nested-rsc-inner' ) ) . toHaveCSS (
117+ 'color' ,
118+ 'rgb(33, 53, 71)' ,
119+ )
120+ await expect ( innerLinks ) . toHaveCount ( initialLinkCount )
121+
122+ // Edit 3: revert — back to original orange.
123+ editor . reset ( )
124+ await expect ( page . locator ( '.test-nested-rsc-inner' ) ) . toHaveCSS (
125+ 'color' ,
126+ 'rgb(255, 165, 0)' ,
127+ )
128+ await expect ( innerLinks ) . toHaveCount ( initialLinkCount )
129+ } )
53130} )
0 commit comments