Skip to content

Commit cbf53d8

Browse files
committed
verify propper <link> cleanup
1 parent 9d0ca52 commit cbf53d8

File tree

1 file changed

+77
-0
lines changed

1 file changed

+77
-0
lines changed

packages/plugin-rsc/e2e/nested-rsc-css-hmr.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)