@tristan2828/ui-foundation 4.42.0 → 4.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ than installing this by hand:
22
22
  | `@tristan2828/ui-foundation/ui/<name>` | shadcn primitives: badge, button, calendar, card, checkbox, combobox, dialog, empty, field, input, input-group, label, popover, select, separator, sheet, sidebar, skeleton, sonner, spinner, switch, table, textarea, tooltip |
23
23
  | `@tristan2828/ui-foundation/gateway` | `safeFetch`, `toAppError`, `networkError` — for an app's `src/api/gateway/` only |
24
24
  | `@tristan2828/ui-foundation/mocks` | MSW: `authHandlers`, `environmentHandlers`, `resetMockAuth`, `getMockCurrentUser`, `MOCK_USER`, `exposeMswForE2E` |
25
- | `@tristan2828/ui-foundation/testing` | Playwright: `defineA11ySuite`, `defineMockModeBannerSuite`, `defineDataEnvironmentBannerSuite`, `forceMswOverride`, `forceLoggedOut`, `waitForMswReady`, `expectDataViewsLoaded` |
25
+ | `@tristan2828/ui-foundation/testing` | Playwright: `test` and `expect` (a page whose `goto()`/`reload()` wait for the app's boot; `withBootWait` for a page of your own), `defineA11ySuite`, `defineMockModeBannerSuite`, `defineDataEnvironmentBannerSuite`, `forceMswOverride`, `forceLoggedOut`, `waitForMswReady`, `expectDataViewsLoaded` |
26
26
  | `@tristan2828/ui-foundation/eslint` | the lint rules, as a flat config: `export default [...uiFoundation()]` |
27
27
  | `@tristan2828/ui-foundation/styles.css` | tokens and base styles, after `@import "tailwindcss";` |
28
28
  | `@tristan2828/ui-foundation/openapi.yaml` | the part of an app's API contract the package calls: `/auth/*`, `/environment` (optional) and the error bodies |
@@ -4,7 +4,7 @@
4
4
  ## Index
5
5
 
6
6
  - [The reference files](#the-reference-files) · lines 37–59
7
- - [Before anything: the entity plan](#before-anything-the-entity-plan) · lines 61–702
7
+ - [Before anything: the entity plan](#before-anything-the-entity-plan) · lines 61–706
8
8
  - Step 1: Add `<Entity>` to `openapi.yaml` · lines 99–106
9
9
  - Step 2: `npm run gen:api` · lines 107–108
10
10
  - Step 3: Add gateway tests in `tests/gateway/<entity>.test.ts`, derived from the spec · lines 109–120
@@ -12,13 +12,13 @@
12
12
  - Step 5: Add a gateway module in `src/api/gateway/<entity>.ts` · lines 131–135
13
13
  - Step 6: Copy the reference files, one for one, not just the two screens · lines 136–598
14
14
  - Step 7: Register routes · lines 599–609
15
- - Step 8: Add Playwright specs for all three screens · lines 610–684
16
- - Step 9: Register the nav entry and route names · lines 685–699
17
- - Step 10: `npm run verify`. · lines 700–702
18
- - [What not to copy](#what-not-to-copy) · lines 704–730
19
- - [After a commit, in the backend](#after-a-commit-in-the-backend) · lines 732–749
20
- - [Deleting](#deleting) · lines 751–780
21
- - [Fields only a script writes](#fields-only-a-script-writes) · lines 782–803
15
+ - Step 8: Add Playwright specs for all three screens · lines 610–688
16
+ - Step 9: Register the nav entry and route names · lines 689–703
17
+ - Step 10: `npm run verify`. · lines 704–706
18
+ - [What not to copy](#what-not-to-copy) · lines 708–734
19
+ - [After a commit, in the backend](#after-a-commit-in-the-backend) · lines 736–753
20
+ - [Deleting](#deleting) · lines 755–784
21
+ - [Fields only a script writes](#fields-only-a-script-writes) · lines 786–807
22
22
 
23
23
  <!-- /index -->
24
24
 
@@ -612,7 +612,11 @@ is a filled-in example while the Widgets demo is still here).
612
612
  MSW overrides — `forceMswOverride`, `waitForMswReady` and the
613
613
  `window.__msw` handle from `@tristan2828/ui-foundation/testing`, and
614
614
  `e2e/widgets-table.spec.ts`/`e2e/widget-form.spec.ts`/
615
- `e2e/widget-view.spec.ts` for the pattern. The view's states are
615
+ `e2e/widget-view.spec.ts` for the pattern. Import `test` and
616
+ `expect` from there too, not from `@playwright/test`: its page's
617
+ `goto()` and `reload()` return once the app has rendered (after MSW's
618
+ start), and load once more if the server refused one of its files,
619
+ so a spec's first expect never meets an empty page. The view's states are
616
620
  loading, not found (a 404: "Not found", the way back, no retry), error
617
621
  (a failed load, then Try again recovering) and success (every section,
618
622
  each value as its cell shows it), plus the sparse record showing its
@@ -1,7 +1,24 @@
1
- import { type Page } from '@playwright/test';
1
+ import { expect, type Page } from '@playwright/test';
2
2
  import { type NavEntry } from '../components/app/app-shell-nav.js';
3
3
  import type { MswOverride } from '../mocks/override.js';
4
4
  export type { MswHandle, MswOverride } from '../mocks/override.js';
5
+ export { expect };
6
+ /**
7
+ * Makes the page's `goto()` and `reload()` return once the app has
8
+ * booted (its first render into `#root`), not at the load event, and load
9
+ * the page once more when one of the app's own files is refused before
10
+ * then; a second refusal fails naming the file. The wait is bounded by
11
+ * the test's timeout like any navigation, and the page's own expects keep
12
+ * their time from there. The `test` exported here applies it to every
13
+ * test's `page`; this is for a page an app's own fixture opens.
14
+ */
15
+ export declare function withBootWait(page: Page): Page;
16
+ /**
17
+ * Playwright's `test`, with a `page` whose `goto()` and `reload()` wait
18
+ * for the app's boot (`withBootWait`). Import `test` and `expect` from
19
+ * here instead of '@playwright/test'; the suites below use it too.
20
+ */
21
+ export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions, import("playwright/test").PlaywrightWorkerArgs & import("playwright/test").PlaywrightWorkerOptions>;
5
22
  /**
6
23
  * Forces a response for the page's *first* load: set before navigation, so
7
24
  * it wins the race against the app's own first fetch. For states forced
@@ -12,8 +12,75 @@
12
12
  // Runs in Node, inside Playwright: @playwright/test and @axe-core/playwright
13
13
  // are optional peer dependencies, needed only by apps that import this.
14
14
  import AxeBuilder from '@axe-core/playwright';
15
- import { expect, test } from '@playwright/test';
15
+ import { expect, test as base } from '@playwright/test';
16
16
  import { navPathname, navTargets } from '../components/app/app-shell-nav.js';
17
+ export { expect };
18
+ // A page load returns at the load event, but the app isn't up yet: React
19
+ // renders only after MSW's worker.start(), which took up to 2.4 s past
20
+ // load in a busy run, and a test's first 5 s expect then failed on an
21
+ // empty page. Under the same load `vite preview` sometimes refuses a
22
+ // connection (about 1 page in 1500): one of the entry's chunks fails, the
23
+ // browser never retries a module script, and the page stays empty until
24
+ // the test's timeout. Both were seen as "never rendered" in every suite.
25
+ const REFUSED = /ERR_CONNECTION_(REFUSED|RESET)/;
26
+ // The app's first render into the template's `<div id="root">`, which is
27
+ // the boot in a mock build (after MSW's start) and a real one alike. A
28
+ // page without one (another site, about:blank) has nothing to wait for.
29
+ const rendered = (page) => page.waitForFunction(() => {
30
+ const root = document.getElementById('root');
31
+ return !root || root.hasChildNodes();
32
+ });
33
+ async function boot(page, load, again) {
34
+ for (let attempt = 1;; attempt++) {
35
+ let onFailed = () => { };
36
+ const refused = new Promise((resolve) => {
37
+ onFailed = (request) => {
38
+ const url = new URL(request.url());
39
+ const own = url.origin === new URL(page.url()).origin;
40
+ if (own && REFUSED.test(request.failure()?.errorText ?? ''))
41
+ resolve(url.pathname);
42
+ };
43
+ page.on('requestfailed', onFailed);
44
+ });
45
+ try {
46
+ const response = await (attempt === 1 ? load() : again());
47
+ const file = await Promise.race([rendered(page).then(() => null), refused]);
48
+ if (file === null)
49
+ return response;
50
+ if (attempt === 2)
51
+ throw new Error(`The server refused ${file} twice: the app never booted`);
52
+ }
53
+ finally {
54
+ page.off('requestfailed', onFailed);
55
+ }
56
+ }
57
+ }
58
+ /**
59
+ * Makes the page's `goto()` and `reload()` return once the app has
60
+ * booted (its first render into `#root`), not at the load event, and load
61
+ * the page once more when one of the app's own files is refused before
62
+ * then; a second refusal fails naming the file. The wait is bounded by
63
+ * the test's timeout like any navigation, and the page's own expects keep
64
+ * their time from there. The `test` exported here applies it to every
65
+ * test's `page`; this is for a page an app's own fixture opens.
66
+ */
67
+ export function withBootWait(page) {
68
+ const goto = page.goto.bind(page);
69
+ const reload = page.reload.bind(page);
70
+ page.goto = (url, options) => boot(page, () => goto(url, options), () => reload(options));
71
+ page.reload = (options) => boot(page, () => reload(options), () => reload(options));
72
+ return page;
73
+ }
74
+ /**
75
+ * Playwright's `test`, with a `page` whose `goto()` and `reload()` wait
76
+ * for the app's boot (`withBootWait`). Import `test` and `expect` from
77
+ * here instead of '@playwright/test'; the suites below use it too.
78
+ */
79
+ export const test = base.extend({
80
+ page: async ({ page }, provide) => {
81
+ await provide(withBootWait(page));
82
+ },
83
+ });
17
84
  /**
18
85
  * Forces a response for the page's *first* load: set before navigation, so
19
86
  * it wins the race against the app's own first fetch. For states forced
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tristan2828/ui-foundation",
3
- "version": "4.42.0",
3
+ "version": "4.43.0",
4
4
  "description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
5
5
  "license": "MIT",
6
6
  "repository": {