dsh-plugin-toolkit 0.8.5 → 0.8.7
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 +15 -0
- package/README.md +22 -1
- package/lib/cli.mjs +3 -0
- package/lib/release.mjs +17 -2
- package/lib/testkit/mount.mjs +29 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## 0.8.7
|
|
2
|
+
|
|
3
|
+
- `panel.aria(selector)` and `panel.expectAria(selector, expected)`: assert what a
|
|
4
|
+
screen reader reads, and get the attribute that differs named in the failure.
|
|
5
|
+
A missing attribute reports `nothing` rather than `""`.
|
|
6
|
+
|
|
7
|
+
## 0.8.6
|
|
8
|
+
|
|
9
|
+
- `release --publish`: run `npm publish` locally instead of waiting for a
|
|
10
|
+
publish workflow. Plenty of small plugins have no release pipeline; before
|
|
11
|
+
this, `release` tagged, pushed and then waited for a registry that would never
|
|
12
|
+
receive the version.
|
|
13
|
+
- The publish failure reports the last line of npm's stderr, so a 403 reads like
|
|
14
|
+
a 403.
|
|
15
|
+
|
|
1
16
|
## 0.8.5
|
|
2
17
|
|
|
3
18
|
- `release --market <dir>`: write the index entry into a checkout of the
|
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:
|
|
@@ -320,6 +332,15 @@ merged
|
|
|
320
332
|
|
|
321
333
|
Without `--pr` it prints the `git add` / `gh pr create` commands instead of running them; `--dry-run` prints the whole sequence before anything happens. A re-run after a failure is safe: the same version with the same publish time is a no-op.
|
|
322
334
|
|
|
335
|
+
### When nothing publishes for you
|
|
336
|
+
|
|
337
|
+
`release` assumes a tag starts a workflow that publishes. Repositories without
|
|
338
|
+
one use `--publish`, which runs `npm publish` in the same step:
|
|
339
|
+
|
|
340
|
+
```sh
|
|
341
|
+
$ dsh-plugin release . --publish
|
|
342
|
+
```
|
|
343
|
+
|
|
323
344
|
### When the index lives in someone else's repository
|
|
324
345
|
|
|
325
346
|
Most plugins are listed in a central market repository, not their own. Point
|
package/lib/cli.mjs
CHANGED
|
@@ -38,6 +38,7 @@ Flags
|
|
|
38
38
|
--dry-run show the steps without touching anything
|
|
39
39
|
--index <path> index file, default market/index.json
|
|
40
40
|
--market <dir> with release: a checkout of the repository that holds the index
|
|
41
|
+
--publish with release: run npm publish here, for repos without a publish workflow
|
|
41
42
|
--tag-prefix <prefix> default plugin-v
|
|
42
43
|
--budget <seconds> how long to wait for npm, default 480
|
|
43
44
|
--pr commit the index on a branch, push it, open the pull request
|
|
@@ -72,6 +73,7 @@ function flags(argv) {
|
|
|
72
73
|
else if (token === '--timeout') parsed.timeout = Number(argv[++index]);
|
|
73
74
|
else if (token === '--pr') parsed.pr = true;
|
|
74
75
|
else if (token === '--merge') parsed.merge = true;
|
|
76
|
+
else if (token === '--publish') parsed.publish = true;
|
|
75
77
|
else if (token === '--base') parsed.base = argv[++index];
|
|
76
78
|
else if (token === '--market') parsed.market = argv[++index];
|
|
77
79
|
else if (token.startsWith('--') === true) throw new Error(`unknown flag ${token}`);
|
|
@@ -213,6 +215,7 @@ export async function run(argv, io = console) {
|
|
|
213
215
|
dir,
|
|
214
216
|
indexFile: options.index,
|
|
215
217
|
marketDir: options.market,
|
|
218
|
+
publish: options.publish === true,
|
|
216
219
|
tagPrefix: options.tagPrefix,
|
|
217
220
|
budgetMs: options.budget === undefined ? undefined : options.budget * 1000,
|
|
218
221
|
pr: options.pr === true,
|
package/lib/release.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* registry, then touch the index — the index entry carries the publish time the
|
|
6
6
|
* registry reported, so writing it earlier would mean guessing.
|
|
7
7
|
*/
|
|
8
|
-
import { run } from './exec.mjs';
|
|
8
|
+
import { run as runCommand } from './exec.mjs';
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
|
|
@@ -14,11 +14,15 @@ import { addVersion, buildEntry, readIndex, writeIndex } from './index-file.mjs'
|
|
|
14
14
|
import { publishedAt, waitForVersion } from './registry.mjs';
|
|
15
15
|
import { defaultGh, mergePr, openIndexPr, waitForChecks } from './pr.mjs';
|
|
16
16
|
|
|
17
|
+
function publishPackage(dir) {
|
|
18
|
+
runCommand('npm', ['publish'], { cwd: dir, stdio: 'pipe' });
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
function git(dir, args) {
|
|
18
22
|
// stderr is dropped on purpose: `rev-parse` on a tag that does not exist yet is
|
|
19
23
|
// an expected miss, and git prints "fatal: ambiguous argument" for it, which
|
|
20
24
|
// reads like a failure in the middle of a successful release.
|
|
21
|
-
return
|
|
25
|
+
return runCommand('git', args, { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export function tagName(version, prefix = 'plugin-v') {
|
|
@@ -122,6 +126,17 @@ export async function release(options) {
|
|
|
122
126
|
gitRun(['tag', tag]);
|
|
123
127
|
gitRun(['push', 'origin', tag]);
|
|
124
128
|
|
|
129
|
+
// Not every plugin publishes from CI. `--publish` runs the publish here, so
|
|
130
|
+
// the same command covers both kinds of repository.
|
|
131
|
+
if (options.publish === true) {
|
|
132
|
+
try {
|
|
133
|
+
(options.publishPackage ?? publishPackage)(dir);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const detail = String(error.stderr ?? error.message).trim().split('\n').slice(-1)[0];
|
|
136
|
+
return { ok: false, stage: 'publish', failures: [{ check: 'publish', message: `npm publish failed: ${detail}` }] };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
125
140
|
const waited = await waitForVersion(name, version, {
|
|
126
141
|
// Wait for the publish *time* as well: `versions` can appear a moment before
|
|
127
142
|
// `time`, and an index entry with a null timestamp fails the market checker.
|
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.7",
|
|
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": {
|