dsh-plugin-toolkit 0.8.6 → 0.8.8
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/CHANGELOG.md +12 -0
- package/README.md +19 -1
- package/lib/dev.mjs +16 -2
- package/lib/testkit/mount.mjs +29 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 0.8.8
|
|
2
|
+
|
|
3
|
+
- `dev` reads the plugin graph the app sends first, so a plugin the app never
|
|
4
|
+
loaded is reported as that ("restart the app once after installing it") instead
|
|
5
|
+
of as a timeout. Found by running `dev` against a real app, not a fake stream.
|
|
6
|
+
|
|
7
|
+
## 0.8.7
|
|
8
|
+
|
|
9
|
+
- `panel.aria(selector)` and `panel.expectAria(selector, expected)`: assert what a
|
|
10
|
+
screen reader reads, and get the attribute that differs named in the failure.
|
|
11
|
+
A missing attribute reports `nothing` rather than `""`.
|
|
12
|
+
|
|
1
13
|
## 0.8.6
|
|
2
14
|
|
|
3
15
|
- `release --publish`: run `npm publish` locally instead of waiting for a
|
package/README.md
CHANGED
|
@@ -135,10 +135,22 @@ First run writes nothing: it tells you to re-run with `UPDATE_SNAPSHOTS=1`, so a
|
|
|
135
135
|
### Check what assistive tech sees
|
|
136
136
|
|
|
137
137
|
```js
|
|
138
|
-
|
|
138
|
+
panel.expectAria('[role="tab"][aria-selected="true"]', { label: 'Protection', selected: 'true' });
|
|
139
139
|
assert.match(panel.must('[role="status"]').textContent, /Score moved 71 → 77/);
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
+
`expectAria()` compares the attributes a screen reader actually reads — `role`,
|
|
143
|
+
`label`, `live`, `selected`, `expanded`, `checked`, `pressed`, `disabled` — and
|
|
144
|
+
names the one that differs:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
[aria-live="polite"]: expected aria text to be "9", but it is "0"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A missing attribute is reported as `nothing`, not as an empty string, because
|
|
151
|
+
those are different bugs. `panel.aria(selector)` returns the same object when you
|
|
152
|
+
want to assert on it yourself.
|
|
153
|
+
|
|
142
154
|
`must()` throws with the rendered markup when the selector misses, which is the difference between "the assertion failed" and "here is what the user actually got".
|
|
143
155
|
|
|
144
156
|
Every example below is a file in this repository, run by CI, and the output shown is the output it prints:
|
|
@@ -205,6 +217,12 @@ watching /work/plugin
|
|
|
205
217
|
|
|
206
218
|
That last line is the whole point: the revision came back from the app's own event stream, so "it reloaded" is a fact, not a hope.
|
|
207
219
|
|
|
220
|
+
A plugin the running app never loaded cannot be reloaded, and the app says so by leaving it out of the graph it sends first. `dev` reads that graph, so a freshly installed plugin gets the sentence that fixes it instead of a timeout:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
copied, but not confirmed: the running app has not loaded demo-panel, so it will not report a reload; restart the app once after installing it
|
|
224
|
+
```
|
|
225
|
+
|
|
208
226
|
| Flag | Meaning |
|
|
209
227
|
|---|---|
|
|
210
228
|
| `--dest <dir>` | write into this installed copy instead of searching `~/.dsh/profiles/*/node_modules/<name>` |
|
package/lib/dev.mjs
CHANGED
|
@@ -61,6 +61,15 @@ export function build({ command, cwd, io = console }) {
|
|
|
61
61
|
export async function verifyReload({ host, id, timeoutMs = 6000, fetchImpl = fetch, action }) {
|
|
62
62
|
const controller = new AbortController();
|
|
63
63
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
64
|
+
// The app sends its plugin graph first. If this plugin is not in it, no amount
|
|
65
|
+
// of waiting will produce a rebuilt frame: it was installed after the app
|
|
66
|
+
// started, and one restart is the fix.
|
|
67
|
+
let loaded = null;
|
|
68
|
+
// Two ways to give up — the stream ended, or it timed out — and both mean the
|
|
69
|
+
// same thing when the app never had this plugin in its graph.
|
|
70
|
+
const miss = (fallback) => (loaded !== null && loaded.has(id) === false
|
|
71
|
+
? `the running app has not loaded ${id}, so it will not report a reload; restart the app once after installing it`
|
|
72
|
+
: fallback);
|
|
64
73
|
const url = `${String(host).replace(/\/+$/, '')}/plugins/events`;
|
|
65
74
|
try {
|
|
66
75
|
const response = await fetchImpl(url, { headers: { accept: 'text/event-stream' }, signal: controller.signal });
|
|
@@ -90,14 +99,19 @@ export async function verifyReload({ host, id, timeoutMs = 6000, fetchImpl = fet
|
|
|
90
99
|
} catch {
|
|
91
100
|
continue;
|
|
92
101
|
}
|
|
102
|
+
if (payload.type === 'graph') {
|
|
103
|
+
const entries = payload.graph?.entries;
|
|
104
|
+
if (Array.isArray(entries) === true) loaded = new Set(entries.map((entry) => entry.id));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
93
107
|
if (payload.type !== 'rebuilt') continue;
|
|
94
108
|
if (JSON.stringify(payload).includes(id) === false) continue;
|
|
95
109
|
return { ok: true, rev: payload.rev ?? null, frame: payload };
|
|
96
110
|
}
|
|
97
111
|
}
|
|
98
|
-
return { ok: false, reason: 'the stream ended before a rebuilt frame arrived' };
|
|
112
|
+
return { ok: false, reason: miss('the stream ended before a rebuilt frame arrived') };
|
|
99
113
|
} catch (error) {
|
|
100
|
-
if (error.name === 'AbortError') return { ok: false, reason: `no rebuilt frame for ${id} within ${timeoutMs}ms` };
|
|
114
|
+
if (error.name === 'AbortError') return { ok: false, reason: miss(`no rebuilt frame for ${id} within ${timeoutMs}ms`) };
|
|
101
115
|
return { ok: false, reason: error.message };
|
|
102
116
|
} finally {
|
|
103
117
|
clearTimeout(timer);
|
package/lib/testkit/mount.mjs
CHANGED
|
@@ -177,6 +177,35 @@ export async function mountClient(options) {
|
|
|
177
177
|
const element = must(selector);
|
|
178
178
|
return Object.fromEntries([...element.attributes].map((attribute) => [attribute.name, attribute.value]));
|
|
179
179
|
},
|
|
180
|
+
// What a screen reader would report, in the same blunt vocabulary as the
|
|
181
|
+
// rest: the attributes, not a re-implementation of the accessibility tree.
|
|
182
|
+
aria: (selector) => {
|
|
183
|
+
const element = must(selector);
|
|
184
|
+
const read = (name) => element.getAttribute(name) ?? undefined;
|
|
185
|
+
return {
|
|
186
|
+
role: read('role'),
|
|
187
|
+
label: read('aria-label'),
|
|
188
|
+
labelledBy: read('aria-labelledby'),
|
|
189
|
+
describedBy: read('aria-describedby'),
|
|
190
|
+
live: read('aria-live'),
|
|
191
|
+
selected: read('aria-selected'),
|
|
192
|
+
expanded: read('aria-expanded'),
|
|
193
|
+
checked: read('aria-checked'),
|
|
194
|
+
pressed: read('aria-pressed'),
|
|
195
|
+
disabled: read('aria-disabled') ?? (element.disabled === true ? 'true' : undefined),
|
|
196
|
+
hidden: read('aria-hidden'),
|
|
197
|
+
text: element.textContent.replace(/\s+/g, ' ').trim(),
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
expectAria: (selector, expected) => {
|
|
201
|
+
const actual = helpers.aria(selector);
|
|
202
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
203
|
+
if (actual[key] === value) continue;
|
|
204
|
+
const shown = actual[key] === undefined ? 'nothing' : `"${actual[key]}"`;
|
|
205
|
+
throw new Error(`${selector}: expected aria ${key} to be "${value}", but it is ${shown}`);
|
|
206
|
+
}
|
|
207
|
+
return actual;
|
|
208
|
+
},
|
|
180
209
|
snapshot: (dir, name) => {
|
|
181
210
|
const result = compare(dir, name, root.innerHTML);
|
|
182
211
|
if (result.status === 'changed') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-toolkit",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
4
4
|
"description": "Preflight, release and test DeepSeek Harness plugins: checks what the host needs, waits for npm to actually serve the version, and keeps the market index honest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|