dsh-panel-control 0.7.2 → 0.9.1

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.
@@ -56,8 +56,24 @@ function check(name, ok, detail) {
56
56
 
57
57
  // The registry currently holds revision "rev-now" for this package; the page
58
58
  // below asks for "rev-old", which is the 404 that silently drops a plugin.
59
+ const PKG_DIR = path.join(STATE, 'pkg-scan');
60
+ await fs.mkdir(PKG_DIR, { recursive: true });
61
+ const pkgFile = (name, body) => {
62
+ const file = path.join(PKG_DIR, name);
63
+ return fs.writeFile(file, body).then(() => file);
64
+ };
65
+ const pkgPaths = {
66
+ 'dsh-panel-control': await pkgFile(
67
+ 'panel.js',
68
+ "const SLOT = 'sidebar.panellist';\nconst KEY = 'panel-control';\nregister({ name: SLOT, id: KEY });\n",
69
+ ),
70
+ dshmarket: await pkgFile('market.js', "register({ name: 'sidebar.panellist', id: 'dsh-market' });\n"),
71
+ 'billion-context': await pkgFile('bili.js', "register({ name: 'settings.section', id: 'bili' });\n"),
72
+ };
73
+
59
74
  const clientModulesStub = {
60
75
  served: [],
76
+ clientPath: (id) => pkgPaths[id] || null,
61
77
  graph: () => ({
62
78
  rev: 'rev-test',
63
79
  release: 'rel',
@@ -65,6 +81,8 @@ const clientModulesStub = {
65
81
  { id: 'a' },
66
82
  { id: 'b' },
67
83
  { id: 'dsh-panel-control', rev: 'rev-now', url: 'plugins/??dsh-panel-control/client.js&rev=rev-now' },
84
+ { id: 'dshmarket', rev: 'rev-now', url: 'plugins/??dshmarket/client.js&rev=rev-now' },
85
+ { id: 'billion-context', rev: 'rev-now', url: 'plugins/??billion-context/client.js&rev=rev-now' },
68
86
  // The plugin that sits next to this one in the real profile: it carries no
69
87
  // fallback of its own, so a retired revision used to drop it silently
70
88
  // while the manager still reported it running.
@@ -185,11 +203,11 @@ if (route) {
185
203
  text: async () => JSON.stringify(payload),
186
204
  json: async () => payload,
187
205
  });
188
- const envelope = (method) => ({
206
+ const envelope = (method, payload = {}) => ({
189
207
  type: 'client-request',
190
208
  rpcId: 'r1',
191
209
  method: 'dsh-panel-control/classify',
192
- payload: { method, payload: {} },
210
+ payload: { method, payload },
193
211
  });
194
212
  try {
195
213
  const response = await route.fetch(request(envelope('inventory')));
@@ -247,9 +265,27 @@ if (route) {
247
265
  check(
248
266
  'inventory ids',
249
267
  Array.isArray(body.result.value.ids) &&
250
- body.result.value.ids.join() === 'a,b,dsh-panel-control,dsh-workspace-control',
268
+ body.result.value.ids.join() === 'a,b,dsh-panel-control,dshmarket,billion-context,dsh-workspace-control',
251
269
  body.result.value && body.result.value.ids.join(),
252
270
  );
271
+ const pkgResponse = await route.fetch(
272
+ request(
273
+ envelope('packages', {
274
+ ids: [
275
+ { id: 'panel-control', slot: 'sidebar.panellist' },
276
+ { id: 'dsh-market', slot: 'sidebar.panellist' },
277
+ { id: 'bili', slot: 'settings.section' },
278
+ { id: 'nobody', slot: 'sidebar.panellist' },
279
+ ],
280
+ }),
281
+ ),
282
+ );
283
+ const pkgBody = await pkgResponse.json();
284
+ const packages = (pkgBody.result && pkgBody.result.value && pkgBody.result.value.packages) || {};
285
+ check('packages maps panel id', packages['panel-control'] === 'dsh-panel-control', JSON.stringify(packages));
286
+ check('packages maps market id', packages['dsh-market'] === 'dshmarket', JSON.stringify(packages));
287
+ check('packages maps settings id', packages.bili === 'billion-context', JSON.stringify(packages));
288
+ check('packages leaves unknown ids unmapped', !packages.nobody, JSON.stringify(packages));
253
289
  } catch (error) {
254
290
  check('inventory answers', false, error.message);
255
291
  }
@@ -27,12 +27,39 @@ for (const marker of [
27
27
  'layoutGuard',
28
28
  'layoutMaxNone',
29
29
  'toggleCollapse',
30
+ 'layoutBridge',
31
+ 'dpc-bridgeBar',
32
+ 'registrantOf',
33
+ 'dpc-nameTag',
34
+ 'data-dpc-label',
35
+ 'applyNames',
36
+ 'layoutTags',
30
37
  ]) {
31
38
  expect(client.includes(marker), 'client.js lost ' + marker);
32
39
  }
33
40
 
41
+ // A bridged row is only as good as the two halves it registers: a keyed main
42
+ // entry the layout can select, and the host-rendered settings section it shows.
43
+ expect(client.includes('BRIDGE_ORDER = 85'), 'bridged rows lost their order slot');
44
+ expect(client.includes('out.bridge = raw.bridge !== false'), 'the bridge switch is not persisted');
45
+ expect(
46
+ client.includes('BRIDGE_SLOT_PREFIX') &&
47
+ client.includes('children: { [slotName]: { kind: \'single\', scope: \'root\' } }') &&
48
+ client.includes('renderSlot(slotName, { close: back }, {})'),
49
+ 'the bridged page no longer renders through its own child slot',
50
+ );
51
+ expect(/\.dpc-bridgeBar\{[^}]*min-height:40px/.test(client), 'the bridge bar lost its 40px row height');
52
+
53
+ // A renamed row keeps its host label addressable while the visible title changes;
54
+ // the original-name tag is opt-out, and the group menu must not sit on transparency.
55
+ expect(client.includes('out.tags = raw.tags !== false'), 'the original-name tag switch is not persisted');
56
+ expect(/\.dpc-nameTag\{[^}]*pointer-events:none/.test(client), 'the original-name tag can intercept clicks');
57
+ expect(/\.dpc-menu\{[^}]*background-color:var\(--dsw-alias-bg-layer-2/.test(client), 'the group menu lost its opaque base');
58
+ expect(pkg.scripts && String(pkg.scripts.check).includes('names-smoke'), 'npm run check no longer runs the names gate');
59
+
34
60
  expect(client.includes("const BUILD_STAMP = 'dsh-panel-control/" + pkg.version + "'"), 'BUILD_STAMP and package.json version disagree');
35
61
  expect(pkg.scripts && String(pkg.scripts.check).includes('layout-smoke'), 'npm run check no longer runs this gate');
62
+ expect(pkg.scripts && String(pkg.scripts.check).includes('bridge-smoke'), 'npm run check no longer runs the bridge gate');
36
63
  expect(/\[data-dpc-list\]\{[^}]*overflow-y:auto/.test(client), 'the panel list is not scrollable');
37
64
  expect(/\[data-dpc-list\]\{[^}]*max-height:max\(120px,var\(--dpc-panellist-max,45vh\)\)/.test(client), 'the panel list lost its height cap');
38
65
  expect(/\[data-dpc-region\]\{[^}]*min-height/.test(client), 'the region below the list lost its floor');
@@ -0,0 +1,425 @@
1
+ // Behaviour gate for custom panel names and the original-name tag.
2
+ //
3
+ // client.js only runs inside the page module loader, so this harness loads the
4
+ // real file in a vm with a tiny DOM and drives the real engine apply(). It
5
+ // covers the parts a static check cannot: mapping renamed rows back to their
6
+ // entries, the tag's placement, the switch, and the restore on dispose.
7
+ import { readFileSync } from 'node:fs';
8
+ import vm from 'node:vm';
9
+
10
+ const SRC = readFileSync(new URL('../client.js', import.meta.url), 'utf8');
11
+
12
+ const stubReact = {
13
+ createElement: (type, props, ...children) => ({ type, props: { ...(props || {}), children } }),
14
+ useState: (value) => [value, () => {}],
15
+ useEffect: () => {},
16
+ useRef: (value) => ({ current: value }),
17
+ useMemo: (fn) => fn(),
18
+ useCallback: (fn) => fn,
19
+ };
20
+
21
+ function makeEl(tag = 'div') {
22
+ const el = {
23
+ tagName: String(tag).toUpperCase(),
24
+ children: [],
25
+ parentElement: null,
26
+ attributes: {},
27
+ style: {},
28
+ className: '',
29
+ textContent: '',
30
+ isConnected: true,
31
+ _width: 260,
32
+ classList: {
33
+ contains: (name) => String(el.className).split(/\s+/).includes(name),
34
+ add() {},
35
+ remove() {},
36
+ },
37
+ setAttribute(key, value) {
38
+ if (key === 'class') el.className = String(value);
39
+ else el.attributes[key] = String(value);
40
+ },
41
+ getAttribute(key) {
42
+ if (key === 'class') return el.className || null;
43
+ return Object.prototype.hasOwnProperty.call(el.attributes, key) ? el.attributes[key] : null;
44
+ },
45
+ removeAttribute(key) {
46
+ delete el.attributes[key];
47
+ },
48
+ hasAttribute(key) {
49
+ return Object.prototype.hasOwnProperty.call(el.attributes, key);
50
+ },
51
+ appendChild(child) {
52
+ child.parentElement = el;
53
+ el.children.push(child);
54
+ return child;
55
+ },
56
+ remove() {
57
+ if (!el.parentElement) return;
58
+ const at = el.parentElement.children.indexOf(el);
59
+ if (at >= 0) el.parentElement.children.splice(at, 1);
60
+ el.parentElement = null;
61
+ },
62
+ addEventListener() {},
63
+ removeEventListener() {},
64
+ getBoundingClientRect() {
65
+ return { width: el._width, height: 40, top: 0, left: 0, right: el._width, bottom: 40 };
66
+ },
67
+ querySelectorAll(selector) {
68
+ return selectAll(el, selector);
69
+ },
70
+ querySelector(selector) {
71
+ return selectAll(el, selector)[0] || null;
72
+ },
73
+ contains(node) {
74
+ if (node === el) return true;
75
+ return el.children.some((child) => child.contains && child.contains(node));
76
+ },
77
+ };
78
+ return el;
79
+ }
80
+
81
+ function descendants(root) {
82
+ const out = [];
83
+ for (const child of root.children) {
84
+ out.push(child);
85
+ out.push(...descendants(child));
86
+ }
87
+ return out;
88
+ }
89
+
90
+ function selectAll(root, selector) {
91
+ const parts = String(selector).split(',').map((part) => part.trim());
92
+ const found = [];
93
+ for (const part of parts) {
94
+ let hits = [];
95
+ if (part === ':scope > button') {
96
+ hits = root.children.filter((child) => child.tagName === 'BUTTON');
97
+ } else if (part === 'button') {
98
+ hits = descendants(root).filter((child) => child.tagName === 'BUTTON');
99
+ } else if (part === '[data-dpc-sep]') {
100
+ hits = descendants(root).filter((child) => child.hasAttribute('data-dpc-sep'));
101
+ } else if (part === '.dpc-nameTag') {
102
+ hits = descendants(root).filter((child) => String(child.className).split(/\s+/).includes('dpc-nameTag'));
103
+ } else if (part === 'button[data-dpc-label]') {
104
+ hits = descendants(root).filter((child) => child.tagName === 'BUTTON' && child.hasAttribute('data-dpc-label'));
105
+ } else {
106
+ const labelled = part.match(/^button\[(data-dpc-label|aria-label)="(.*)"\]$/);
107
+ if (labelled) {
108
+ hits = descendants(root).filter(
109
+ (child) => child.tagName === 'BUTTON' && child.getAttribute(labelled[1]) === labelled[2],
110
+ );
111
+ }
112
+ }
113
+ for (const hit of hits) if (!found.includes(hit)) found.push(hit);
114
+ }
115
+ return found;
116
+ }
117
+
118
+ function makeButton(label, withTitle = true) {
119
+ const button = makeEl('button');
120
+ button.setAttribute('aria-label', label);
121
+ const glyph = makeEl('span');
122
+ glyph.setAttribute('aria-hidden', 'true');
123
+ button.appendChild(glyph);
124
+ if (withTitle) {
125
+ const title = makeEl('span');
126
+ title.className = 'panelTitle';
127
+ title.textContent = label;
128
+ button.appendChild(title);
129
+ }
130
+ return button;
131
+ }
132
+
133
+ function makeList(buttons, width = 260) {
134
+ const list = makeEl('div');
135
+ list.className = 'panelList-test';
136
+ list._width = width;
137
+ const parent = makeEl('div');
138
+ parent.appendChild(list);
139
+ parent.appendChild(makeEl('div'));
140
+ for (const button of buttons) list.appendChild(button);
141
+ return { list, parent };
142
+ }
143
+
144
+ function makeWindow(localPrefs) {
145
+ const memory = new Map();
146
+ if (localPrefs !== undefined) memory.set('dsh.panel-control.v1', JSON.stringify(localPrefs));
147
+ return {
148
+ document: makeDocument(),
149
+ localStorage: {
150
+ getItem: (key) => (memory.has(key) ? memory.get(key) : null),
151
+ setItem: (key, value) => memory.set(key, String(value)),
152
+ removeItem: (key) => memory.delete(key),
153
+ },
154
+ navigator: { language: 'zh-CN' },
155
+ location: { reload() {} },
156
+ CSS: { escape: (value) => String(value) },
157
+ addEventListener() {},
158
+ removeEventListener() {},
159
+ __DSH_BOOT__: {
160
+ rev: 'names-test',
161
+ entries: [],
162
+ ids: ['plugins', 'dsh-market', 'workspace-control', 'panel-control', 'dup-a', 'dup-b'],
163
+ },
164
+ __ModuleLoader__: { def: null, load(def) { this.def = def; } },
165
+ };
166
+ }
167
+
168
+ function makeDocument() {
169
+ const doc = {
170
+ _list: null,
171
+ readyState: 'loading',
172
+ head: makeEl('head'),
173
+ body: makeEl('body'),
174
+ documentElement: makeEl('html'),
175
+ title: '',
176
+ visibilityState: 'visible',
177
+ createElement: (tag) => makeEl(tag),
178
+ createTextNode: () => ({ textContent: '' }),
179
+ createDocumentFragment: () => makeEl('div'),
180
+ querySelectorAll(selector) {
181
+ if (selector === '[class*="panelList"]') return doc._list ? [doc._list] : [];
182
+ if (selector === 'style[data-dpc-style]') return [];
183
+ return doc._list ? selectAll(doc._list, selector) : [];
184
+ },
185
+ querySelector(selector) {
186
+ return doc.querySelectorAll(selector)[0] || null;
187
+ },
188
+ getElementById: () => null,
189
+ addEventListener() {},
190
+ removeEventListener() {},
191
+ };
192
+ return doc;
193
+ }
194
+
195
+ const calls = { selectPanel: [], effects: [] };
196
+ const PACKAGES = {
197
+ plugins: '@deepseek-ai/dsh-client-ui-plugin-manager',
198
+ 'dsh-market': 'dshmarket',
199
+ 'panel-control': 'dsh-panel-control',
200
+ 'dup-a': 'dsh-dup-a',
201
+ 'dup-b': 'dsh-dup-b',
202
+ };
203
+ const layout = {
204
+ panelInfo: { getSnapshot: () => ({ activePanelId: null }) },
205
+ selectPanel(id) {
206
+ calls.selectPanel.push(id);
207
+ },
208
+ };
209
+ const locale = { getLocale: () => ({ active: 'zh-CN' }), register: () => {}, subscribe: () => () => {} };
210
+
211
+ function makeCtx(registries) {
212
+ const makeRegistry = (name) => {
213
+ if (!registries.has(name)) registries.set(name, []);
214
+ return registries.get(name);
215
+ };
216
+ const subscribers = new Map();
217
+ const notify = (name) => {
218
+ for (const callback of subscribers.get(name) || []) Promise.resolve().then(callback);
219
+ };
220
+ const slotService = {
221
+ register(options, component) {
222
+ const record = { options, component, registrant: options.registrant || 'dsh-panel-control' };
223
+ makeRegistry(options.name).push(record);
224
+ notify(options.name);
225
+ return () => {
226
+ const all = makeRegistry(options.name);
227
+ const at = all.indexOf(record);
228
+ if (at >= 0) all.splice(at, 1);
229
+ notify(options.name);
230
+ };
231
+ },
232
+ entriesOfSlot(name) {
233
+ return makeRegistry(name).slice();
234
+ },
235
+ inject(name, callback) {
236
+ return callback();
237
+ },
238
+ subscribe(name, callback) {
239
+ if (!subscribers.has(name)) subscribers.set(name, []);
240
+ subscribers.get(name).push(callback);
241
+ return () => {
242
+ subscribers.set(name, (subscribers.get(name) || []).filter((fn) => fn !== callback));
243
+ };
244
+ },
245
+ renderSlot(name, props, opts) {
246
+ return stubReact.createElement('div', { 'data-slot': name, 'data-only': opts && opts.only });
247
+ },
248
+ };
249
+ return {
250
+ registries,
251
+ ctx: {
252
+ fiber: { name: 'dsh-panel-control' },
253
+ get(name) {
254
+ if (name === 'layout') return layout;
255
+ if (name === 'locale') return locale;
256
+ return undefined;
257
+ },
258
+ effect(fn) {
259
+ const dispose = fn();
260
+ if (typeof dispose === 'function') calls.effects.push(dispose);
261
+ return dispose;
262
+ },
263
+ slots: slotService,
264
+ connection: {
265
+ rpc: {
266
+ async call(_channel, _endpoint, message) {
267
+ if (message && message.method === 'packages') {
268
+ const ids = (message.payload && message.payload.ids) || [];
269
+ const packages = {};
270
+ for (const row of ids) if (PACKAGES[row.id]) packages[row.id] = PACKAGES[row.id];
271
+ return { ok: true, value: { packages, rev: 'names-test' } };
272
+ }
273
+ return { ok: true, value: {} };
274
+ },
275
+ },
276
+ },
277
+ },
278
+ };
279
+ }
280
+
281
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
282
+
283
+ function seed(registries, buttons) {
284
+ const rows = [
285
+ ['plugins', '插件', 0],
286
+ ['dsh-market', '插件市场', 1],
287
+ ['workspace-control', '工作区管理', 20],
288
+ ];
289
+ for (const [id, label, order] of rows) {
290
+ registries.get('sidebar.panellist').push({
291
+ options: { name: 'sidebar.panellist', id, order, label },
292
+ component: () => null,
293
+ registrant: 'tk',
294
+ });
295
+ }
296
+ return rows.map(([, label]) => label);
297
+ }
298
+
299
+ async function run(localPrefs, options = {}) {
300
+ calls.effects.length = 0;
301
+ calls.selectPanel.length = 0;
302
+ const registries = new Map();
303
+ const doc = makeDocument();
304
+ const buttons = (options.labels || ['插件', '插件市场', '工作区管理', '面板管理']).map((label) =>
305
+ makeButton(label, options.withTitle !== false),
306
+ );
307
+ const { list } = makeList(buttons, options.width || 260);
308
+ doc._list = list;
309
+ const win = makeWindow(localPrefs);
310
+ win.document = doc;
311
+ const app = makeCtx(registries);
312
+ registries.set('sidebar.panellist', []);
313
+ seed(registries, buttons);
314
+ if (options.extraEntries) {
315
+ for (const [id, label, order] of options.extraEntries) {
316
+ registries.get('sidebar.panellist').push({
317
+ options: { name: 'sidebar.panellist', id, order, label },
318
+ component: () => null,
319
+ registrant: 'tk',
320
+ });
321
+ }
322
+ }
323
+
324
+ globalThis.window = win;
325
+ globalThis.document = doc;
326
+ globalThis.localStorage = win.localStorage;
327
+ globalThis.CSS = win.CSS;
328
+ win.__ModuleLoader__.def = null;
329
+ vm.runInThisContext(SRC, { filename: 'client.js' });
330
+ const mod = win.__ModuleLoader__.def.factory((name) => {
331
+ if (name === 'react') return stubReact;
332
+ throw new Error('unexpected require: ' + name);
333
+ });
334
+ mod.apply(app.ctx);
335
+ await sleep(options.wait || 420);
336
+ return { buttons, list, calls, ctx: app.ctx };
337
+ }
338
+
339
+ const failures = [];
340
+ const check = (ok, message) => {
341
+ if (!ok) failures.push(message);
342
+ };
343
+ const titleOf = (button) => button.children.find((child) => child.tagName === 'SPAN' && String(child.className) !== 'dpc-nameTag' && child.getAttribute('aria-hidden') !== 'true');
344
+ const tagOf = (button) => button.children.find((child) => String(child.className).split(/\s+/).includes('dpc-nameTag'));
345
+
346
+ // 1. A renamed entry: custom text, original label addressable, one tag.
347
+ const renamed = await run({ version: 2, names: { plugins: '我的插件' }, tags: true });
348
+ const first = renamed.buttons[0];
349
+ check(first.getAttribute('aria-label') === '我的插件', 'the renamed row kept its old aria-label');
350
+ check(first.getAttribute('data-dpc-label') === '插件', 'the original label was not kept for mapping');
351
+ check(titleOf(first) && titleOf(first).textContent === '我的插件', 'the visible title was not renamed');
352
+ const tag = tagOf(first);
353
+ check(Boolean(tag), 'the renamed row has no package tag');
354
+ check(tag && tag.textContent === 'plugin-manager', 'the tag does not contain the package name');
355
+ check(tag && tag.getAttribute('aria-hidden') === 'true', 'the tag is not hidden from assistive tech');
356
+ check(!tagOf(renamed.buttons[1]), 'an unrenamed row grew a tag');
357
+ check(first.style.order !== undefined, 'the engine did not apply the row order');
358
+
359
+ // 2. The switch removes the tag but keeps the custom name.
360
+ const noTags = await run({ version: 2, names: { plugins: '我的插件' }, tags: false });
361
+ check(noTags.buttons[0].getAttribute('aria-label') === '我的插件', 'turning tags off also dropped the name');
362
+ check(!tagOf(noTags.buttons[0]), 'the tag switch did not remove the tag');
363
+
364
+ // 3. Clearing the name restores the host label.
365
+ const cleared = await run({ version: 2, names: {}, tags: true });
366
+ check(cleared.buttons[0].getAttribute('aria-label') === '插件', 'a cleared name did not restore the host label');
367
+ check(titleOf(cleared.buttons[0]).textContent === '插件', 'a cleared name did not restore the title');
368
+ check(!tagOf(cleared.buttons[0]), 'a cleared name left the tag behind');
369
+
370
+ // 4. Icon rail: no title span, no tag, but the accessible name still follows.
371
+ const rail = await run({ version: 2, names: { plugins: '我的插件' }, tags: true }, { width: 40, withTitle: false });
372
+ check(rail.buttons[0].getAttribute('aria-label') === '我的插件', 'the rail row did not get the custom name');
373
+ check(!tagOf(rail.buttons[0]), 'the rail row grew a tag');
374
+
375
+ // 5. Duplicate labels: positional mapping still reaches both rows, and only the
376
+ // renamed one changes.
377
+ const dup = await run(
378
+ { version: 2, names: { 'dup-a': '甲' }, tags: true },
379
+ {
380
+ labels: ['插件', '插件市场', '工作区管理', '面板管理', '相同', '相同'],
381
+ extraEntries: [
382
+ ['dup-a', '相同', 30],
383
+ ['dup-b', '相同', 31],
384
+ ],
385
+ },
386
+ );
387
+ const dupButtons = dup.buttons.slice(-2);
388
+ check(dupButtons[0].getAttribute('aria-label') === '甲', 'the first duplicate-label row was not renamed');
389
+ check(dupButtons[1].getAttribute('aria-label') === '相同', 'the second duplicate-label row was renamed by mistake');
390
+ check(tagOf(dupButtons[0]).textContent === 'dup-a', 'the duplicate row tag did not use its package');
391
+
392
+ // 5b. An id the Host cannot map falls back to the id itself, with `dsh-` gone.
393
+ const unmapped = await run({ version: 2, names: { 'workspace-control': '工作区' }, tags: true });
394
+ check(tagOf(unmapped.buttons[2]).textContent === 'workspace-control', 'an unmapped package did not fall back to its id');
395
+
396
+ // 6. Dispose restores the host DOM.
397
+ const disposed = await run({ version: 2, names: { plugins: '我的插件' }, tags: true });
398
+ for (const dispose of disposed.calls.effects.slice().reverse()) dispose();
399
+ check(disposed.buttons[0].getAttribute('aria-label') === '插件', 'dispose did not restore the host aria-label');
400
+ check(titleOf(disposed.buttons[0]).textContent === '插件', 'dispose did not restore the host title');
401
+ check(!tagOf(disposed.buttons[0]), 'dispose left the original-name tag behind');
402
+ check(!disposed.buttons[0].getAttribute('data-dpc-label'), 'dispose left the original-label marker behind');
403
+
404
+ console.log(
405
+ JSON.stringify(
406
+ {
407
+ renamed: Boolean(tag),
408
+ tag: tag ? tag.textContent : null,
409
+ tagsOff: !tagOf(noTags.buttons[0]),
410
+ restored: cleared.buttons[0].getAttribute('aria-label'),
411
+ railTag: Boolean(tagOf(rail.buttons[0])),
412
+ duplicates: dupButtons.map((button) => button.getAttribute('aria-label')),
413
+ unmappedTag: tagOf(unmapped.buttons[2]) ? tagOf(unmapped.buttons[2]).textContent : null,
414
+ },
415
+ null,
416
+ 2,
417
+ ),
418
+ );
419
+ if (failures.length) {
420
+ console.error('names-smoke: ' + failures.length + ' problem(s)');
421
+ for (const failure of failures) console.error(' - ' + failure);
422
+ process.exit(1);
423
+ }
424
+ console.log('names-smoke: ok');
425
+ process.exit(0);