Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions e2e/instant-marketing-hard-nav.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { instant } from "@next/playwright";
import { type APIRequestContext, expect, test } from "@playwright/test";

import { dismissCookieBannerIfVisible } from "./helpers/dismissCookieBanner";

/** Instant MPA mode includes prerendered marketing copy only with a production PPR shell. */
async function hasProductionInstantShell(
request: APIRequestContext,
path: string,
expectedSnippet: string,
): Promise<boolean> {
const response = await request.get(path, {
headers: { cookie: "next-instant-navigation-testing=1" },
});
if (!response.ok()) {
return false;
}
const html = await response.text();
return html.includes(expectedSnippet);
}

/**
* L5: hard navigation (MPA reload) instant shell for default-locale marketing.
* Skips under `next dev` (instant MPA bails to CSR); runs on production / preview builds.
*/
test.describe("instant marketing hard navigation (L5)", () => {
test.describe.configure({ mode: "serial" });

test.beforeAll(async ({ request }) => {
const shellReady = await hasProductionInstantShell(
request,
"/",
"Your code, frame by frame.",
);
test.skip(
!shellReady,
"Requires production PPR shell (instant MPA CSR-bails in next dev)",
);
});

test("home hero shell is instant on hard navigation", async ({ page }) => {
test.setTimeout(120_000);

await page.goto("/");
await dismissCookieBannerIfVisible(page);

await instant(page, async () => {
await page.reload({ waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { level: 1 })).toContainText(
"Your code, frame by frame.",
{ timeout: 30_000 },
);
});
});

test("privacy shell is instant on hard navigation", async ({ page }) => {
test.setTimeout(120_000);

await page.goto("/privacy");
await dismissCookieBannerIfVisible(page);

await instant(page, async () => {
await page.reload({ waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { level: 1 })).toHaveText(
"Privacy Policy",
{ timeout: 30_000 },
);
});
});

test("daily shell is instant on hard navigation", async ({ page }) => {
test.setTimeout(120_000);

await page.goto("/daily");
await dismissCookieBannerIfVisible(page);

await instant(page, async () => {
await page.reload({ waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { level: 4 })).toContainText(
"Not sure what to solve?",
{ timeout: 30_000 },
);
});
});
});
2 changes: 1 addition & 1 deletion vibe-docs/Instant-Navigations-Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ Visited routes can stay mounted in hidden Activity boundaries. Heavy clients mus
| Pyodide worker | `pythonRunner.release()` via `usePlaygroundRuntimeRelease` | Playground |
| Shared primitive | `useDeferredClientMount(onCleanup?)` | Reuse for future widgets |

E2e: `home-webgl-nav`, `playground-monaco-nav`, `home-hero-preview-nav`.
Hard navigation (L5): production PPR shell validated by `instant-marketing-hard-nav` e2e (skips under `next dev`); `DocumentLocaleSync` keeps `lang`/`dir` aligned after hydration.

---

Expand Down
3 changes: 2 additions & 1 deletion vibe-docs/Instant-Navigations-TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@
- [x] Activity lifecycle shells: `WebGLCanvasShell`, `CodeRunner`, shared `useDeferredClientMount`
- [x] E2e: landing WebGL recovery, playground Monaco nav, Pyodide `release()` on playground leave, hero preview after instant nav
- [x] `pythonRunner.release()` on playground Activity hide; benchmark RAF throttle cancel on unmount
- [ ] Initial page-load instant shell (hard navigation) — follow-up
- [x] Initial page-load instant shell (hard navigation) — `e2e/instant-marketing-hard-nav.spec.ts` (prod/preview PPR; skips in dev)
- [ ] Session / device hints in Suspense for fuller marketing instant shell — follow-up
- [ ] Playground/profile instant adoption (optional)
5 changes: 3 additions & 2 deletions vibe-docs/Locale-Migration-Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ Pages `i18n` auto-redirects `/en/*` → unprefixed URLs, so **`next.config` rewr
2. ~~Root `headers()` locale read~~ — direct read in `RootHtmlShell` with root `instant = false` (correct RTL; no Suspense fallback flash).
3. ~~Single provider mount + cached i18n~~ — one `AppRootLayoutClient`; `'use cache'` on translations; locale layouts `instant = false`.
4. ~~`@next/playwright` `instant()` tests~~ — marketing client navigations (`e2e/instant-marketing-nav.spec.ts`).
5. Initial page-load instant shell (hard navigation) — follow-up.
6. Playground/profile instant adoption — optional.
5. ~~Initial page-load instant shell (hard navigation)~~ — static `RootHtmlShell`, `DocumentLocaleSync`, hard-nav e2e (auto-skips in dev).
6. Session / device hints in Suspense for fuller marketing shell — optional follow-up.
7. Playground/profile instant adoption — optional.

---

Expand Down
Loading