@jsenv/navi 0.29.63 → 0.29.65

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.
@@ -138,6 +138,12 @@ consistency across the app, not from any single call site.
138
138
  `<Interpolate>` for one sentence, `createI18n` for the app's registry,
139
139
  `naviI18n` for navi's own texts. Read it before writing a user-visible
140
140
  sentence, and before overriding a navi message.
141
+ - `docs/testid.md` — how a test names an element: why role + accessible name
142
+ comes first, when `data-testid` takes over, and where navi puts it — on the
143
+ control's host (the real `<input>`/`<button>`), not on the box around it.
144
+ Read it before writing a selector in a Playwright/Cypress test against a navi
145
+ app, and before targeting a `.navi_*` class or a `navi-*`/`data-*` attribute
146
+ of navi's own.
141
147
  - `docs/interactions.md` — the `interactions` prop: making a component answer a
142
148
  swipe, a held press, a shortcut, and registering a gesture navi does not have.
143
149
  Read it before reading the pointer by hand — who owns a press between nested
@@ -121,9 +121,9 @@ positioned relative to its anchor, say what the anchor is:
121
121
  ```
122
122
 
123
123
  The `anchor` prop always wins over whatever the command carried.
124
- `anchorCustomEventDetail="ignore"` (Popover only) goes further and drops the
125
- event's anchor entirely, for a popover that must never be anchored to whatever
126
- opened it.
124
+ `anchorCustomEventDetail="ignore"` goes further and drops the event's anchor
125
+ entirely for a popover that must never be anchored to whatever opened it, and
126
+ for a dialog that must never be sized from it (`SidePanel` needs both).
127
127
 
128
128
  ## Opening it ON something
129
129
 
@@ -214,48 +214,39 @@ refuses the one close that matters, the one over a control mid-action.
214
214
 
215
215
  ### Closing when a button also runs an action
216
216
 
217
- Closing from inside the `action` does not close. While the action runs, the
218
- button that started it is busy, and a busy control is exactly what a popup
219
- refuses to close over (see the top of this page). The refusal is not silent —
220
- the busy control raises a callout saying so — but the popup stays open, and the
221
- first Escape afterwards dismisses that callout rather than the popup, which
222
- reads as a popup that no longer closes at all.
223
-
224
- There are two shapes, and they say different things:
217
+ A button that carries both runs them in that order: the action first, the
218
+ command once it succeeded.
225
219
 
226
220
  ```jsx
227
- // Closes on the press. The popup does NOT wait for save(): it is already
228
- // closed when the action starts, and the action finishes behind it.
221
+ // Stays open while save() runs, closes when it resolves, stays open if it
222
+ // throws with what was typed still there and the error on the button.
229
223
  <Button command="--navi-close" commandfor="note-dialog" action={save}>
230
224
  Save
231
225
  </Button>
232
226
  ```
233
227
 
234
- `command` next to `action` is the one to reach for when the answer is taken as
235
- soon as it is given — the popup gets out of the way, the save runs on its own.
236
- Know what it costs: **a save that fails does so behind a closed popup**, and the
237
- error callout it raises lands on a button nobody can see any more. Use it where
238
- the failure is reported somewhere else, or where losing it is acceptable.
239
-
240
- ```jsx
241
- // Closes only once save() has resolved, and stays open if it throws.
242
- <Button
243
- action={save}
244
- onActionEnd={() => {
245
- // NOT synchronously: the button still counts as busy while its own
246
- // action-end handlers run, and the popup would refuse the close.
247
- queueMicrotask(() => {
248
- triggerNaviCommand(dialogRef.current, "--navi-close");
249
- });
250
- }}
251
- >
252
- Save
253
- </Button>
254
- ```
255
-
256
- `onActionEnd` only fires when the action succeeded, so the popup stays open on
257
- failure — the answer is then neither committed nor given up, and it is still
258
- there to be corrected, with the error shown on the button that raised it.
228
+ The command is what the press means AFTER the work, so it waits for the work:
229
+ closing first would take the form off the screen over a request that can still
230
+ fail, and the error callout it raises would land on a button nobody can see any
231
+ more. An action that ends in an error or an abort a `confirm` answered "no" is
232
+ an abort leaves the popup where it is. Same rule a form already follows for
233
+ what comes after its send (`data-after-send`, see `resolveAfterSend` in
234
+ commands.js).
235
+
236
+ The wait is why closing **from inside** the action still does not close: while
237
+ the action runs, the button that started it is busy, and a busy control is
238
+ exactly what a popup refuses to close over (see the top of this page). The
239
+ refusal is not silent — the busy control raises a callout saying so — but the
240
+ popup stays open, and the first Escape afterwards dismisses that callout rather
241
+ than the popup, which reads as a popup that no longer closes at all. Nothing has
242
+ to be hand-written to work around it: `command` next to `action` is that
243
+ workaround, done at the one moment where the action has settled and the button
244
+ is no longer busy.
245
+
246
+ To close on the press instead — the answer taken as soon as it is given, the
247
+ save running on its own behind a closed popup — close from somewhere the action
248
+ does not hold up, e.g. an `onClick` of your own. Know what it costs: **a save
249
+ that fails does so behind a closed popup**.
259
250
 
260
251
  ## Escape cancels, the other gestures keep
261
252
 
package/docs/testid.md ADDED
@@ -0,0 +1,103 @@
1
+ # Naming an element for a test (`data-testid`)
2
+
3
+ What we want: **a test names what it targets, not how it is drawn.** A test
4
+ that finds a button through `.navi_button_content > span:nth-child(2)`, or
5
+ through the sentence written in it, breaks the day the component is restyled,
6
+ the wording changes, or the app is translated — and it breaks with a failure
7
+ that says nothing about the application. `data-testid` is the name the test
8
+ uses: an attribute that exists for no other reason, so nothing but the test can
9
+ break it.
10
+
11
+ ```jsx
12
+ <Button data-testid="save-game" action={save}>
13
+ Save
14
+ </Button>
15
+ ```
16
+
17
+ ```js
18
+ // playwright
19
+ await page.getByTestId("save-game").click();
20
+ // cypress
21
+ cy.get('[data-testid="save-game"]').click();
22
+ ```
23
+
24
+ `data-testid` is that exact spelling on purpose: it is Playwright's default
25
+ `testIdAttribute` and Testing Library's default, and it is the one navi knows
26
+ about (see below). Cypress has no built-in attribute — it reads it as a plain
27
+ selector — so the same name works there too. Don't invent `data-test`,
28
+ `data-cy` or `data-qa` variants per project.
29
+
30
+ ## Prefer the contract the user already sees
31
+
32
+ `data-testid` is not the first tool. When an element has a role and a stable
33
+ accessible name, target THAT:
34
+
35
+ ```js
36
+ await page.getByRole("button", { name: "Save" }).click();
37
+ await page.getByLabel("Email").fill("a@b.c");
38
+ ```
39
+
40
+ It asserts something the application actually owes the user — a button that is
41
+ a button, a field that is labelled — so the test fails when accessibility
42
+ regresses, which a `data-testid` never notices. Reach for `data-testid` when
43
+ that contract is absent or not usable:
44
+
45
+ - there is no accessible name, or it is an icon, or it is dynamic;
46
+ - the app is translated, and the test must pass in every language;
47
+ - several elements legitimately share role and name (rows of a list, cells of a
48
+ table) and only their position tells them apart — a per-item
49
+ `data-testid={`row-${id}`}` names them by identity instead of by index.
50
+
51
+ ## Where it lands in navi
52
+
53
+ Navi renders more than one element per component: a control is a little tree —
54
+ a wrapper box, sometimes a label, slots, and inside it the real
55
+ `<input>`/`<button>`/`<select>` that holds the value and receives the clicks
56
+ (the **host**, marked `navi-control-host` in the DOM; the root of the tree
57
+ carries `navi-control`).
58
+
59
+ **`data-testid` lands on the host**, not on the wrapper. That is deliberate and
60
+ it is the whole point: `getByTestId("email")` gives back the element you can
61
+ `fill()`, `check()`, `press()`, and assert `toBeDisabled()` / `toHaveValue()`
62
+ on. A testid on the wrapper would answer a click with a hit on the padding.
63
+
64
+ ```jsx
65
+ <Input name="email" data-testid="email" />
66
+ ```
67
+
68
+ ```html
69
+ <span class="navi_input" navi-control="input">
70
+ <input navi-control-host="input" data-testid="email" />
71
+ </span>
72
+ ```
73
+
74
+ This routing is what `CONTROL_ATTRIBUTE_SET` in
75
+ `src/control/control_context.js` lists: the props navi hands to the host rather
76
+ than to the box around it (`id`, `name`, `type`, `value`, `tabIndex`,
77
+ `data-testid`, …). Any prop navi does not know lands on the root box instead —
78
+ so a `data-test-id` or a `data-qa` of your own would name the wrapper, which is
79
+ the second reason to keep the standard spelling.
80
+
81
+ When the wrapper IS what the test wants — a whole field with its label and its
82
+ error message, a section, a row — put the testid on the surrounding component
83
+ (`<Field data-testid="email-field">`, `<Box data-testid="cart-row">`); anything
84
+ built on `Box` forwards it to its own element.
85
+
86
+ ## What not to target
87
+
88
+ Navi's own attributes are implementation, not a contract: `data-header`,
89
+ `data-body`, `data-scrollable`, `data-variant`, `data-callout-*`,
90
+ `navi-control*`, `.navi_*` class names, and the ids navi generates when none is
91
+ given (`useId`). They change without notice and without a migration note.
92
+ Likewise a `view-transition-name` or a CSS variable: those exist to draw, not
93
+ to be found.
94
+
95
+ ## Naming
96
+
97
+ The name says what the thing is in the application, never how it looks or where
98
+ it sits: `save-game`, `player-row`, `cart-total` — not `blue-button`,
99
+ `second-input`, `header-btn`. It is a public name of the app for its tests: when
100
+ it has to change, it is because the feature changed.
101
+
102
+ `data-testid` stays in the production DOM — navi strips nothing. That is
103
+ accepted: a few bytes per element, against tests that survive a redesign.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.63",
3
+ "version": "0.29.65",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {
@@ -29,7 +29,7 @@
29
29
  "prepublishOnly": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@jsenv/dom": "0.17.21",
32
+ "@jsenv/dom": "0.17.22",
33
33
  "@jsenv/humanize": "1.7.8",
34
34
  "@jsenv/validity": "0.4.2"
35
35
  },