@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.
- package/dist/jsenv_navi.js +333 -87
- package/dist/jsenv_navi.js.map +30 -22
- package/docs/AI_INSTRUCTIONS.md +6 -0
- package/docs/popup_open.md +29 -38
- package/docs/testid.md +103 -0
- package/package.json +2 -2
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -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
|
package/docs/popup_open.md
CHANGED
|
@@ -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"`
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
//
|
|
228
|
-
//
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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.
|
|
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.
|
|
32
|
+
"@jsenv/dom": "0.17.22",
|
|
33
33
|
"@jsenv/humanize": "1.7.8",
|
|
34
34
|
"@jsenv/validity": "0.4.2"
|
|
35
35
|
},
|