Skip to content

Commit 7219ea3

Browse files
Brooooooklynclaude
andcommitted
test(solid): cover island hydration on a page without frontmatter
The client must render the frontmatter provider even when the page has no frontmatter: every createComponent call opens a hydration context, so skipping the provider shifts the island's hydration keys and it never hydrates. Add a production case for that page and say why in the client entry. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent ace42b6 commit 7219ea3

2 files changed

Lines changed: 36 additions & 5 deletions

File tree

‎packages/solid/src/plugin-islands.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,9 @@ function generateIslandClientEntry(
413413
lines.push('};');
414414

415415
// Each wrapper scopes its SSR keys independently of its page and layout.
416-
// Recreate the same provider tree before hydrating that island's root.
416+
// Recreate the same provider tree before hydrating that island's root. Every
417+
// createComponent call opens a hydration context, so the provider stays even
418+
// when the page has no frontmatter.
417419
lines.push(`
418420
async function mountIsland(el, mod, props, frontmatter) {
419421
const { hydrate } = await import("solid-js/web");

‎packages/solid/test/integration/island-hydration.test.ts‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ export default function Page() {
6666
<VisibleCounter name="visible-second" initial={40} />
6767
<script innerHTML={${JSON.stringify(capture)}} />
6868
</main>;
69+
}`,
70+
'pages/plain.island.tsx': `
71+
import Counter from '../components/Counter' with { island: 'load' };
72+
export default function Plain() {
73+
return <main>
74+
<Counter name="plain" initial={60} />
75+
<script innerHTML={${JSON.stringify(capture)}} />
76+
</main>;
6977
}`,
7078
};
7179

@@ -115,7 +123,11 @@ async function visitProductionFixture(
115123
}
116124
}
117125

118-
async function expectReused(visit: IslandPageVisit, name: string): Promise<void> {
126+
async function expectReused(
127+
visit: IslandPageVisit,
128+
name: string,
129+
frontmatter: string | null = 'Island metadata',
130+
): Promise<void> {
119131
const { page } = visit;
120132
await page.waitForFunction(
121133
(name) =>
@@ -142,7 +154,7 @@ async function expectReused(visit: IslandPageVisit, name: string): Promise<void>
142154
input: true,
143155
value: `draft-${name}`,
144156
labelled: true,
145-
frontmatter: 'Island metadata',
157+
frontmatter,
146158
});
147159
}
148160

@@ -175,9 +187,9 @@ describe.each(['dev', 'production'])('%s island hydration', (mode) => {
175187
productionBuild?.cleanup();
176188
});
177189

178-
function newFixture() {
190+
function newFixture(path = '') {
179191
return mode === 'production'
180-
? visitProductionFixture(productionBrowser!, productionServer!.resolvedUrls!.local[0])
192+
? visitProductionFixture(productionBrowser!, productionServer!.resolvedUrls!.local[0] + path)
181193
: visitFixture();
182194
}
183195

@@ -232,6 +244,23 @@ describe.each(['dev', 'production'])('%s island hydration', (mode) => {
232244
}
233245
});
234246

247+
// The dev helper serves only the index page.
248+
it.runIf(mode === 'production')(
249+
'reuses SSR DOM on an island page without frontmatter',
250+
async () => {
251+
const fixture = await newFixture('plain');
252+
try {
253+
await expectReused(fixture.visit, 'plain', null);
254+
const button = fixture.visit.page.locator('[data-counter="plain"] button');
255+
await button.click();
256+
await expect.poll(() => button.textContent()).toBe('61');
257+
expect(fixture.visit.diagnostics()).not.toContain('pageerror');
258+
} finally {
259+
await fixture.close();
260+
}
261+
},
262+
);
263+
235264
it('hydrates an async resource inside Suspense without replacing the resolved SSR DOM', async () => {
236265
const fixture = await newFixture();
237266
try {

0 commit comments

Comments
 (0)