diff --git a/docs/e2e-testing.md b/docs/e2e-testing.md index 1a60d32ce..8299af776 100644 --- a/docs/e2e-testing.md +++ b/docs/e2e-testing.md @@ -42,6 +42,33 @@ before the first `make dev` run. uv run pytest -m e2e tests/e2e ``` +## Inertia v3 fixture pages + +The framework's own pages never use partial reloads, deferred, merge or `once` +props, infinite scroll, error bags, history encryption, `WhenVisible`, polling +or prefetch. The dev host therefore ships fixture pages that do, so +`tests/e2e/inertia/` can drive those client features in a real +browser: + +- Routes: [`host/e2e_inertia/`](../host/e2e_inertia/) under + `/__e2e/inertia/*`, plus loader counters at `GET /__e2e/inertia/_probes` + (reset with `POST /__e2e/inertia/_probes/reset`). +- Pages: `host/client_app/pages/E2eInertia/*.tsx`, rendered as + `E2eInertia/`. They are marked `i18n-exempt-file` and never shown to + users. +- Gating: `host/main.py` mounts the router only when `SM_ENVIRONMENT` is + `development`, and every route needs a signed-in user. In any other + environment the paths do not exist (`host/tests/test_e2e_inertia_gating.py`). +- Fixtures: `tests/e2e/inertia/conftest.py` (scoped to that directory, so every other e2e test keeps the plain `page`) signs in once per session through + the API and fails a test on any console or page error. Mark a test + `allow_resource_errors` when it provokes a 4xx/5xx on purpose. + +Run them against a development server (Vite required, as for every e2e test): + +```bash +uv run pytest -m e2e tests/e2e/inertia +``` + ## Configuration When you write e2e tests, read these environment variables (all optional): diff --git a/host/client_app/pages.ts b/host/client_app/pages.ts index 7bd776878..ace4ab5ae 100644 --- a/host/client_app/pages.ts +++ b/host/client_app/pages.ts @@ -22,11 +22,17 @@ type PageLoader = () => Promise; // into the production bundle, shipping the test and dragging testing-library // into a vendor chunk. Mirrors TEST_FILE_SUFFIXES in the manifest generator, // which emits the same negations for module pages. -const hostPages = import.meta.glob([ - './pages/**/*.tsx', - '!./pages/**/*.test.tsx', - '!./pages/**/*.spec.tsx', -]); +const hostPages = { + ...import.meta.glob([ + './pages/**/*.tsx', + '!./pages/**/*.test.tsx', + '!./pages/**/*.spec.tsx', + '!./pages/E2eInertia/**', + ]), + // The e2e fixture pages back routes the host mounts only in development + // (host/e2e_inertia); a production build constant-folds DEV and drops them. + ...(import.meta.env.DEV ? import.meta.glob(['./pages/E2eInertia/*.tsx']) : {}), +}; const pages: Record = {}; diff --git a/host/client_app/pages/E2eInertia/Deferred.tsx b/host/client_app/pages/E2eInertia/Deferred.tsx new file mode 100644 index 000000000..1a32881a8 --- /dev/null +++ b/host/client_app/pages/E2eInertia/Deferred.tsx @@ -0,0 +1,23 @@ +// i18n-exempt-file: e2e fixture page, never shown to users +import { Deferred as InertiaDeferred } from '@inertiajs/react'; +import { E2ePage, Value } from './components/E2ePage'; + +export default function Deferred({ fast, slow }: { fast?: number; slow?: number }) { + return ( + + Loading…

}> + +
+ Loading…

}> + +
+ Loading…

} + rescue={

Loader failed safely

} + > +

Loader ready

+
+
+ ); +} diff --git a/host/client_app/pages/E2eInertia/Forms.tsx b/host/client_app/pages/E2eInertia/Forms.tsx new file mode 100644 index 000000000..e73d2b424 --- /dev/null +++ b/host/client_app/pages/E2eInertia/Forms.tsx @@ -0,0 +1,68 @@ +// i18n-exempt-file: e2e fixture page, never shown to users +import { useForm } from '@inertiajs/react'; +import { buttonClass, E2ePage } from './components/E2ePage'; + +function DemoForm({ bag }: { bag: 'default' | 'second' }) { + const form = useForm({ name: '', email: '' }); + return ( +
{ + event.preventDefault(); + form.post(`/__e2e/inertia/forms/${bag}`, { + errorBag: bag === 'second' ? 'second' : undefined, + }); + }} + > +

{bag === 'default' ? 'Default form' : 'Second form'}

+ {(['name', 'email'] as const).map((field) => ( +
+ + form.setData(field, event.target.value)} + /> + +
+ ))} + + {bag === 'default' && ( + + )} +
+ ); +} + +export default function Forms({ success }: { success: string }) { + return ( + +

{success ? `Saved ${success} form` : ''}

+ + +
+ ); +} diff --git a/host/client_app/pages/E2eInertia/History.tsx b/host/client_app/pages/E2eInertia/History.tsx new file mode 100644 index 000000000..999b9ebf6 --- /dev/null +++ b/host/client_app/pages/E2eInertia/History.tsx @@ -0,0 +1,43 @@ +// i18n-exempt-file: e2e fixture page, never shown to users +import { Link, router } from '@inertiajs/react'; +import { buttonClass, E2ePage, Value } from './components/E2ePage'; + +export default function History({ mode }: { mode: string }) { + return ( + + + +