dsh-background-by-model 0.4.3 → 0.4.5
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/README.md +12 -0
- package/README.zh.md +12 -0
- package/lib/client.js +56 -31
- package/lib/client.js.map +1 -1
- package/lib/{host-scan-WjK2VGBe.js → host-scan-C6rdxkf8.js} +20 -16
- package/lib/host-scan.js +1 -1
- package/lib/index.js +1 -1
- package/lib/testing.js +36 -15
- package/package.json +1 -1
- package/test/host-check.test.mjs +232 -36
|
@@ -219,7 +219,8 @@ const HOST_CONTRACTS = [
|
|
|
219
219
|
checks: [{
|
|
220
220
|
kind: "selector",
|
|
221
221
|
selector: "div[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]",
|
|
222
|
-
optionalWhen: "
|
|
222
|
+
optionalWhen: "settingsOpen",
|
|
223
|
+
visible: true
|
|
223
224
|
}],
|
|
224
225
|
sources: [{
|
|
225
226
|
path: "dsh-client-ui-primitives/lib/index.js",
|
|
@@ -275,7 +276,7 @@ const HOST_CONTRACTS = [
|
|
|
275
276
|
checks: [{
|
|
276
277
|
kind: "attr",
|
|
277
278
|
attr: "data-composer-card",
|
|
278
|
-
optionalWhen: "
|
|
279
|
+
optionalWhen: "chatHasMessages"
|
|
279
280
|
}],
|
|
280
281
|
sources: [{
|
|
281
282
|
path: "dsh-client-ui-conversation/lib/client.js",
|
|
@@ -301,7 +302,7 @@ const HOST_CONTRACTS = [
|
|
|
301
302
|
checks: [{
|
|
302
303
|
kind: "attr",
|
|
303
304
|
attr: "data-cordis-panel",
|
|
304
|
-
optionalWhen: "
|
|
305
|
+
optionalWhen: "cordisMounted"
|
|
305
306
|
}],
|
|
306
307
|
sources: [{
|
|
307
308
|
path: "dsh-client-ui-cordis/lib/client.js",
|
|
@@ -327,7 +328,8 @@ const HOST_CONTRACTS = [
|
|
|
327
328
|
checks: [{
|
|
328
329
|
kind: "selector",
|
|
329
330
|
selector: "div[data-sidebar-right-panel]",
|
|
330
|
-
optionalWhen: "
|
|
331
|
+
optionalWhen: "rightPanelMounted",
|
|
332
|
+
visible: true
|
|
331
333
|
}],
|
|
332
334
|
sources: [{
|
|
333
335
|
path: `${SIDEBAR_RIGHT}/lib/client.js`,
|
|
@@ -377,9 +379,10 @@ const HOST_CONTRACTS = [
|
|
|
377
379
|
},
|
|
378
380
|
target: "[data-dockkit-content]",
|
|
379
381
|
checks: [{
|
|
380
|
-
kind: "
|
|
381
|
-
|
|
382
|
-
|
|
382
|
+
kind: "selector",
|
|
383
|
+
selector: "[data-dockkit-content]",
|
|
384
|
+
optionalWhen: "dockHasTab",
|
|
385
|
+
visible: true
|
|
383
386
|
}],
|
|
384
387
|
sources: [{
|
|
385
388
|
path: ".",
|
|
@@ -404,9 +407,9 @@ const HOST_CONTRACTS = [
|
|
|
404
407
|
},
|
|
405
408
|
target: "[data-dockkit-empty]",
|
|
406
409
|
checks: [{
|
|
407
|
-
kind: "
|
|
408
|
-
|
|
409
|
-
|
|
410
|
+
kind: "selector",
|
|
411
|
+
selector: "[data-dockkit-empty]",
|
|
412
|
+
optionalWhen: "dockHasNoTab"
|
|
410
413
|
}],
|
|
411
414
|
sources: [{
|
|
412
415
|
path: `${SIDEBAR_RIGHT}/lib/client.js`,
|
|
@@ -430,9 +433,9 @@ const HOST_CONTRACTS = [
|
|
|
430
433
|
},
|
|
431
434
|
target: "[data-dockkit-float]",
|
|
432
435
|
checks: [{
|
|
433
|
-
kind: "
|
|
434
|
-
|
|
435
|
-
|
|
436
|
+
kind: "selector",
|
|
437
|
+
selector: "[data-dockkit-float]",
|
|
438
|
+
optionalWhen: "paneFloated"
|
|
436
439
|
}],
|
|
437
440
|
sources: [{
|
|
438
441
|
path: `${SIDEBAR_RIGHT}/lib/client.js`,
|
|
@@ -458,7 +461,8 @@ const HOST_CONTRACTS = [
|
|
|
458
461
|
checks: [{
|
|
459
462
|
kind: "selector",
|
|
460
463
|
selector: "[data-chat-flow]",
|
|
461
|
-
optionalWhen: "
|
|
464
|
+
optionalWhen: "chatHasMessages",
|
|
465
|
+
visible: true
|
|
462
466
|
}],
|
|
463
467
|
sources: [{
|
|
464
468
|
path: "dsh-client-ui-chat/lib/client.js",
|
|
@@ -484,7 +488,7 @@ const HOST_CONTRACTS = [
|
|
|
484
488
|
checks: [{
|
|
485
489
|
kind: "selector",
|
|
486
490
|
selector: "[data-conversation-scroll]",
|
|
487
|
-
optionalWhen: "
|
|
491
|
+
optionalWhen: "chatHasMessages"
|
|
488
492
|
}],
|
|
489
493
|
sources: [{
|
|
490
494
|
path: "dsh-client-ui-conversation/lib/client.js",
|
|
@@ -510,7 +514,7 @@ const HOST_CONTRACTS = [
|
|
|
510
514
|
checks: [{
|
|
511
515
|
kind: "selector",
|
|
512
516
|
selector: "[data-conversation-composer-overlay]",
|
|
513
|
-
optionalWhen: "
|
|
517
|
+
optionalWhen: "trajectoryMounted"
|
|
514
518
|
}],
|
|
515
519
|
sources: [{
|
|
516
520
|
path: "dsh-client-ui-trajectory/lib/client.js",
|
package/lib/host-scan.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as thisPluginDir, c as DSH_FLOOR, i as resolveHostRoots, l as HOST_CONTRACTS, n as listHostPackages, o as versionOf, r as meetsFloor, t as checkHostOnDisk } from "./host-scan-
|
|
1
|
+
import { a as thisPluginDir, c as DSH_FLOOR, i as resolveHostRoots, l as HOST_CONTRACTS, n as listHostPackages, o as versionOf, r as meetsFloor, t as checkHostOnDisk } from "./host-scan-C6rdxkf8.js";
|
|
2
2
|
export { DSH_FLOOR, HOST_CONTRACTS, checkHostOnDisk, listHostPackages, meetsFloor, resolveHostRoots, thisPluginDir, versionOf };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as checkHostOnDisk } from "./host-scan-
|
|
1
|
+
import { t as checkHostOnDisk } from "./host-scan-C6rdxkf8.js";
|
|
2
2
|
import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
package/lib/testing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as DSH_FLOOR, d as reportToMarkdown, f as symptomOf, i as resolveHostRoots, l as HOST_CONTRACTS, o as versionOf, p as verdictOf, r as meetsFloor, s as CONTRACT_GROUPS, t as checkHostOnDisk, u as labelOf } from "./host-scan-
|
|
1
|
+
import { c as DSH_FLOOR, d as reportToMarkdown, f as symptomOf, i as resolveHostRoots, l as HOST_CONTRACTS, o as versionOf, p as verdictOf, r as meetsFloor, s as CONTRACT_GROUPS, t as checkHostOnDisk, u as labelOf } from "./host-scan-C6rdxkf8.js";
|
|
2
2
|
const UI_CSS = `
|
|
3
3
|
/* The section is rendered INLINE inside the host settings dialog's content
|
|
4
4
|
* column: the host provides the modal chrome (backdrop, centering, closing).
|
|
@@ -288,24 +288,43 @@ function browserEnv(ctx) {
|
|
|
288
288
|
if (host === null) return "";
|
|
289
289
|
return getComputedStyle(host).getPropertyValue(token).trim();
|
|
290
290
|
},
|
|
291
|
+
visible: (el) => {
|
|
292
|
+
if (el === null || typeof getComputedStyle !== "function") return true;
|
|
293
|
+
try {
|
|
294
|
+
return getComputedStyle(el).visibility !== "hidden";
|
|
295
|
+
} catch {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
},
|
|
291
299
|
styleSheets: () => typeof document === "undefined" ? [] : Array.from(document.styleSheets),
|
|
292
300
|
ctx
|
|
293
301
|
};
|
|
294
302
|
}
|
|
295
303
|
const ANCHORS = {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
304
|
+
chatHasMessages: "[data-chat-turn]",
|
|
305
|
+
trajectoryMounted: "[data-conversation-composer-overlay]",
|
|
306
|
+
rightPanelMounted: "div[data-sidebar-right-panel],[data-dockkit-strip],[data-dockkit-pane]",
|
|
307
|
+
cordisMounted: "[data-cordis-panel]",
|
|
308
|
+
dockHasTab: "[data-dockkit-tab]",
|
|
309
|
+
dockHasNoTab: "[data-dockkit-empty],[data-dockkit-add-tab]",
|
|
310
|
+
paneFloated: "[data-dockkit-float],[data-sidebar-right-mode=\"float\"]",
|
|
311
|
+
settingsOpen: "div[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]"
|
|
301
312
|
};
|
|
302
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Whether an anchor's surface is on screen.
|
|
315
|
+
*
|
|
316
|
+
* `optionalWhen: X` skips the check while this is FALSE — "the feature is not
|
|
317
|
+
* observable right now", which is what the field name promises. Every anchor is
|
|
318
|
+
* therefore written as a POSITIVE predicate (`dockHasTab`, `chatHasMessages`), and
|
|
319
|
+
* an anchor whose name reads as an absence would silently invert its check.
|
|
320
|
+
*/
|
|
321
|
+
function anchorMounted(env, anchorId) {
|
|
303
322
|
const sel = ANCHORS[anchorId];
|
|
304
323
|
if (sel === void 0) return true;
|
|
305
324
|
try {
|
|
306
325
|
return env.querySelector(sel) !== null;
|
|
307
326
|
} catch {
|
|
308
|
-
return
|
|
327
|
+
return true;
|
|
309
328
|
}
|
|
310
329
|
}
|
|
311
330
|
/** Walk a dotted path off a root object, tolerating a missing first hop. */
|
|
@@ -371,7 +390,9 @@ function ruleMentioned(env, literal, side) {
|
|
|
371
390
|
}
|
|
372
391
|
return false;
|
|
373
392
|
}
|
|
374
|
-
/**
|
|
393
|
+
/**
|
|
394
|
+
* Which sides of stylesheet the probe can actually read (a blind check must not fail).
|
|
395
|
+
*/
|
|
375
396
|
function readability(env) {
|
|
376
397
|
const out = {
|
|
377
398
|
host: false,
|
|
@@ -451,14 +472,14 @@ function evaluate(env, check) {
|
|
|
451
472
|
};
|
|
452
473
|
}
|
|
453
474
|
case "selector": {
|
|
454
|
-
const
|
|
475
|
+
const el = env.querySelector(check.selector);
|
|
455
476
|
const name = `querySelector(${JSON.stringify(check.selector)})`;
|
|
456
|
-
if (
|
|
477
|
+
if (el !== null && (check.visible !== true || env.visible(el))) return {
|
|
457
478
|
status: "pass",
|
|
458
479
|
name,
|
|
459
480
|
detail: "element found"
|
|
460
481
|
};
|
|
461
|
-
if (check.optionalWhen !== void 0 && !
|
|
482
|
+
if (check.optionalWhen !== void 0 && !anchorMounted(env, check.optionalWhen)) return {
|
|
462
483
|
status: "skip",
|
|
463
484
|
name,
|
|
464
485
|
detail: "not on screen right now"
|
|
@@ -466,8 +487,8 @@ function evaluate(env, check) {
|
|
|
466
487
|
return {
|
|
467
488
|
status: "fail",
|
|
468
489
|
name,
|
|
469
|
-
detail: "no element matches",
|
|
470
|
-
reason: `no element matches ${check.selector} while its surface is on screen`
|
|
490
|
+
detail: el === null ? "no element matches" : "the element is hidden",
|
|
491
|
+
reason: el === null ? `no element matches ${check.selector} while its surface is on screen` : `${check.selector} is in the DOM but hidden while its surface is on screen`
|
|
471
492
|
};
|
|
472
493
|
}
|
|
473
494
|
case "attr": {
|
|
@@ -478,7 +499,7 @@ function evaluate(env, check) {
|
|
|
478
499
|
name,
|
|
479
500
|
detail: "attribute found"
|
|
480
501
|
};
|
|
481
|
-
if (check.optionalWhen !== void 0 && !
|
|
502
|
+
if (check.optionalWhen !== void 0 && !anchorMounted(env, check.optionalWhen)) return {
|
|
482
503
|
status: "skip",
|
|
483
504
|
name,
|
|
484
505
|
detail: "not on screen right now"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-background-by-model",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Appearance plugin for DeepSeek Harness: an ordered list of model rules, each carrying its own wallpaper, theme color, layout mode, opacity and blur 鈥?the background follows the model you switch to.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
package/test/host-check.test.mjs
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The host self-check's own tests
|
|
2
|
+
* The host self-check's own tests 鈥?run with
|
|
3
3
|
*
|
|
4
4
|
* pnpm test
|
|
5
5
|
*
|
|
6
6
|
* Three things are covered, and they are the three that can rot silently:
|
|
7
7
|
*
|
|
8
|
-
* 1. **The contract table holds together**
|
|
8
|
+
* 1. **The contract table holds together** 鈥?unique ids, machine-checkable
|
|
9
9
|
* sources, a known criticality, and at least one check or an explicit
|
|
10
10
|
* "documentation only" marker. A contract point added without a source would
|
|
11
11
|
* otherwise be invisible to the offline scan.
|
|
12
|
-
* 2. **The probe reports what is actually there**
|
|
12
|
+
* 2. **The probe reports what is actually there** 鈥?the whole probe is driven
|
|
13
13
|
* against a fixture written in the shape of a host DOM, so "the host renamed
|
|
14
14
|
* `--dsw-specific-menu`" produces exactly the failure the panel claims, and a
|
|
15
15
|
* surface that is merely off screen produces `n/a` rather than a false alarm.
|
|
16
|
-
* 3. **The UI stylesheet, and the one piece of version logic**
|
|
16
|
+
* 3. **The UI stylesheet, and the one piece of version logic** 鈥?every `dab-*`
|
|
17
17
|
* class a component uses is defined in it, and the floor comparison is exact
|
|
18
18
|
* on the release triple.
|
|
19
19
|
*
|
|
@@ -36,7 +36,7 @@ const { HOST_CONTRACTS, DSH_FLOOR, probeContracts, verdictOf, meetsFloor, UI_CSS
|
|
|
36
36
|
|
|
37
37
|
const { readFile, readdir } = await import('node:fs/promises')
|
|
38
38
|
|
|
39
|
-
//
|
|
39
|
+
// 鈹€鈹€ 1. the contract table 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
40
40
|
|
|
41
41
|
test('contract ids are unique and non-empty', () => {
|
|
42
42
|
const seen = new Set()
|
|
@@ -75,30 +75,46 @@ test('critical contracts carry a check that can fail', () => {
|
|
|
75
75
|
}
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
//
|
|
78
|
+
// 鈹€鈹€ 2. the probe, against a fixture in the shape of a host DOM 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
79
79
|
|
|
80
80
|
/** A DOM stand-in with just enough selector support for the contract checks. */
|
|
81
81
|
function makeDom(host) {
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
/**
|
|
83
|
+
* `selector 鈫?present`, including attribute and comma-OR forms. A descendant
|
|
84
|
+
* selector is satisfied when every compound in it is present 鈥?the stand-in has
|
|
85
|
+
* no tree, and the contracts are written so that "the elements exist" is the
|
|
86
|
+
* question being asked.
|
|
87
|
+
*/
|
|
88
|
+
const one = part => {
|
|
89
|
+
// `*` is the universal selector: always satisfied.
|
|
90
|
+
if (part === '*') return true
|
|
91
|
+
if (host.has(part)) return true
|
|
92
|
+
const attr = /^\[([a-z-]+)\]$/.exec(part)
|
|
93
|
+
if (attr !== null && host.has(`[${attr[1]}]`)) return true
|
|
89
94
|
return false
|
|
90
95
|
}
|
|
96
|
+
const matches = sel => sel.split(',').map(p => p.trim()).some(
|
|
97
|
+
part => part.split(/\s+/).every(one),
|
|
98
|
+
)
|
|
91
99
|
return {
|
|
92
|
-
|
|
100
|
+
// The element remembers which selector found it: a stand-in has no box to
|
|
101
|
+
// measure, and several real attributes live on ONE element (the dock's tab
|
|
102
|
+
// host carries both the populated marker and the parked empty seat), so
|
|
103
|
+
// "is this element visible" has to be answered per element, not per fixture.
|
|
104
|
+
querySelector: sel => (matches(sel) ? { style: {}, foundBy: sel } : null),
|
|
93
105
|
// Where the host publishes its palette: `body` in the real client, and the
|
|
94
106
|
// fixture models exactly that. Reading the document element instead is what
|
|
95
107
|
// made seven healthy tokens report as empty.
|
|
96
108
|
tokenRoot: () => ({ style: {} }),
|
|
97
109
|
// `on` is null for an unscoped token check (the probe reads it off the token
|
|
98
110
|
// root) and the resolved element otherwise. The fixture answers from its own
|
|
99
|
-
// table either way
|
|
111
|
+
// table either way 鈥?whether a value is readable is the host's own business,
|
|
100
112
|
// and that is exactly what the table stands in for.
|
|
101
113
|
computedValue: (token, _on) => (host.get(`computed:${token}`) ?? ''),
|
|
114
|
+
// A stand-in has no layout, so visibility is answered from an explicit table
|
|
115
|
+
// (`hidden:<selector>`). Anything unlisted counts as on screen 鈥?the
|
|
116
|
+
// conservative direction: an unanswerable question never fails a check.
|
|
117
|
+
visible: el => el === null || !host.has(`hidden:${el.foundBy}`),
|
|
102
118
|
styleSheets: () => [],
|
|
103
119
|
}
|
|
104
120
|
}
|
|
@@ -111,7 +127,11 @@ const CORE_PRESENT = [
|
|
|
111
127
|
'[data-composer-card]',
|
|
112
128
|
'[data-cordis-panel]',
|
|
113
129
|
'[data-chat-flow]',
|
|
130
|
+
'[data-chat-turn]',
|
|
114
131
|
'[data-conversation-scroll]',
|
|
132
|
+
// The session root, so "no chat view" and "a chat view with nothing in it" can
|
|
133
|
+
// be told apart in the fixture the way they are in the host.
|
|
134
|
+
'[data-conversation-session]',
|
|
115
135
|
'[data-conversation-composer-overlay]',
|
|
116
136
|
'.dab-root',
|
|
117
137
|
'[data-dockkit-strip]',
|
|
@@ -132,8 +152,8 @@ const PRESENT_TOKENS = [
|
|
|
132
152
|
|
|
133
153
|
/**
|
|
134
154
|
* The literals the HOST's stylesheets declare. The two halves of a token
|
|
135
|
-
* contract ask different questions
|
|
136
|
-
* name", `computed` = "does it resolve to a value"
|
|
155
|
+
* contract ask different questions 鈥?`rule` = "does the host still declare this
|
|
156
|
+
* name", `computed` = "does it resolve to a value" 鈥?so both are listed.
|
|
137
157
|
*/
|
|
138
158
|
const HOST_RULES = [
|
|
139
159
|
'--dsw-menu-surface-fill', '--dsw-specific-menu', '--dsw-specific-sidebar-fill',
|
|
@@ -149,7 +169,7 @@ const OWN_RULES = ['.dab-card']
|
|
|
149
169
|
/**
|
|
150
170
|
* A stylesheet the probe may read.
|
|
151
171
|
*
|
|
152
|
-
* `own: true` writes this plugin's marker
|
|
172
|
+
* `own: true` writes this plugin's marker 鈥?the ONLY thing that tells our sheets
|
|
153
173
|
* apart from the host's, since the host's own CSS modules set `data-plugin` on
|
|
154
174
|
* theirs, so a probe keying off that skips the entire host theme.
|
|
155
175
|
*
|
|
@@ -185,19 +205,50 @@ function hostStyles(literals = HOST_RULES, ownLiterals = OWN_RULES) {
|
|
|
185
205
|
/**
|
|
186
206
|
* A fixture host: `selectors` are the ones that match, `rules` the literals the
|
|
187
207
|
* host stylesheet declares, and every token in `PRESENT_TOKENS` resolves unless
|
|
188
|
-
* the caller drops it.
|
|
208
|
+
* the caller drops it. `hidden` lists selectors that are present but parked off
|
|
209
|
+
* screen (`visibility:hidden`) 鈥?the state the dock's empty seat sits in.
|
|
189
210
|
*/
|
|
190
|
-
function hostEnv({ ctx, selectors = CORE_PRESENT, rules = HOST_RULES, tokens = PRESENT_TOKENS }) {
|
|
211
|
+
function hostEnv({ ctx, selectors = CORE_PRESENT, rules = HOST_RULES, tokens = PRESENT_TOKENS, hidden = [] }) {
|
|
191
212
|
// A Map, not a Set: the fixture must answer with the token's VALUE, and only a
|
|
192
213
|
// Map carries one (a Set has no `get`).
|
|
193
214
|
const host = new Map()
|
|
194
215
|
for (const s of selectors) host.set(s, true)
|
|
216
|
+
for (const s of hidden) host.set(`hidden:${s}`, true)
|
|
195
217
|
for (const t of tokens) host.set(`computed:${t}`, '#123456')
|
|
196
218
|
return { ...makeDom(host), styleSheets: () => hostStyles(rules), ctx }
|
|
197
219
|
}
|
|
198
220
|
|
|
199
|
-
/**
|
|
200
|
-
|
|
221
|
+
/**
|
|
222
|
+
* The two states the dock alternates between. In the real DOM the tab host and
|
|
223
|
+
* the empty seat are different elements, and each contract's anchor is what says
|
|
224
|
+
* whether its own attribute can be observed — so the fixture provides the anchor
|
|
225
|
+
* for every contract it expects to answer, and omits the anchors of the ones it
|
|
226
|
+
* expects to read as n/a.
|
|
227
|
+
*
|
|
228
|
+
* The panel selector is listed twice on purpose: the fixture matches selectors by
|
|
229
|
+
* exact text (it has no tree to walk), and the contracts spell it both as
|
|
230
|
+
* `[data-sidebar-right-panel]` (the anchor) and `div[data-sidebar-right-panel]`
|
|
231
|
+
* (the check).
|
|
232
|
+
*/
|
|
233
|
+
const DOCK_POPULATED = [
|
|
234
|
+
'[data-sidebar-right-panel]',
|
|
235
|
+
'div[data-sidebar-right-panel]',
|
|
236
|
+
'[data-dockkit-tab]',
|
|
237
|
+
'[data-dockkit-content]',
|
|
238
|
+
'[data-dockkit-empty]',
|
|
239
|
+
]
|
|
240
|
+
const DOCK_EMPTY = [
|
|
241
|
+
'[data-sidebar-right-panel]',
|
|
242
|
+
'div[data-sidebar-right-panel]',
|
|
243
|
+
'[data-dockkit-empty]',
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
/** The healthy fixtures: nothing missing, every service published. */
|
|
247
|
+
const healthyEnv = (dock = DOCK_POPULATED, extra = {}) => hostEnv({
|
|
248
|
+
...extra,
|
|
249
|
+
ctx: healthyCtx(),
|
|
250
|
+
selectors: [...CORE_PRESENT, ...dock],
|
|
251
|
+
})
|
|
201
252
|
|
|
202
253
|
/** A context publishing the four services the model hop needs. */
|
|
203
254
|
function healthyCtx(overrides = {}) {
|
|
@@ -211,11 +262,14 @@ function healthyCtx(overrides = {}) {
|
|
|
211
262
|
return { get: id => services[id] }
|
|
212
263
|
}
|
|
213
264
|
|
|
214
|
-
test('a healthy host passes every contract', () => {
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
265
|
+
test('a healthy host passes every contract, in either dock state', () => {
|
|
266
|
+
for (const [name, dock] of [['populated tab', DOCK_POPULATED], ['empty seat', DOCK_EMPTY]]) {
|
|
267
|
+
const results = probeContracts(healthyEnv(dock), 'en')
|
|
268
|
+
const failed = results.filter(r => r.status === 'fail')
|
|
269
|
+
assert.deepEqual(failed.map(r => r.id), [],
|
|
270
|
+
`unexpected failures with a ${name}: ${failed.map(r => `${r.id}: ${r.reason}`).join('; ')}`)
|
|
271
|
+
assert.equal(verdictOf({ results, summary: { pass: results.length, fail: 0, skip: 0, total: results.length } }), 'ok')
|
|
272
|
+
}
|
|
219
273
|
})
|
|
220
274
|
|
|
221
275
|
test('a renamed menu token fails exactly that contract, and names the symptom', () => {
|
|
@@ -232,8 +286,8 @@ test('a renamed menu token fails exactly that contract, and names the symptom',
|
|
|
232
286
|
|
|
233
287
|
test('a renamed token the host still aliases is caught by the rule half', () => {
|
|
234
288
|
// The 0.1.7 menu split in reverse: the host keeps the OLD name working (aliased)
|
|
235
|
-
// but stops declaring the new one. `computed` still answers
|
|
236
|
-
// how that bug stayed silent
|
|
289
|
+
// but stops declaring the new one. `computed` still answers 鈥?which is exactly
|
|
290
|
+
// how that bug stayed silent 鈥?so the `rule` half is the only thing that can
|
|
237
291
|
// tell "renamed" from "still there".
|
|
238
292
|
const env = hostEnv({
|
|
239
293
|
ctx: healthyCtx(),
|
|
@@ -248,7 +302,7 @@ test('a renamed token the host still aliases is caught by the rule half', () =>
|
|
|
248
302
|
|
|
249
303
|
test('a host that stops publishing the palette fails the computed half', () => {
|
|
250
304
|
// The other failure shape: the name is still declared somewhere, but body no
|
|
251
|
-
// longer carries a value
|
|
305
|
+
// longer carries a value 鈥?the case that made 7 healthy tokens look empty when
|
|
252
306
|
// the probe read the wrong element.
|
|
253
307
|
const env = hostEnv({
|
|
254
308
|
ctx: healthyCtx(),
|
|
@@ -270,7 +324,7 @@ test('a dropped session service fails the model hop', () => {
|
|
|
270
324
|
test('the plugin\'s own token stylesheet cannot make a host check pass', () => {
|
|
271
325
|
// The situation that produced the 0.1.7 white band: the plugin re-emits the
|
|
272
326
|
// menu fill itself, so its own sheet declares the token. That re-emission must
|
|
273
|
-
// never be accepted as evidence about the HOST
|
|
327
|
+
// never be accepted as evidence about the HOST 鈥?otherwise the check is green
|
|
274
328
|
// by construction on exactly the host where the token was renamed away.
|
|
275
329
|
const env = hostEnv({
|
|
276
330
|
ctx: healthyCtx(),
|
|
@@ -287,7 +341,7 @@ test('the plugin\'s own token stylesheet cannot make a host check pass', () => {
|
|
|
287
341
|
assert.equal(byId('token.menuSurface').status, 'fail')
|
|
288
342
|
assert.match(byId('token.menuSurface').reason, /no host stylesheet declares/)
|
|
289
343
|
assert.equal(byId('token.menuAlias').status, 'fail')
|
|
290
|
-
//
|
|
344
|
+
// 鈥hile the contract whose other half IS our own sheet still passes: this is
|
|
291
345
|
// what `side: 'own'` is for, and why the two sides must never be merged.
|
|
292
346
|
assert.equal(byId('settings.card').status, 'pass')
|
|
293
347
|
})
|
|
@@ -304,9 +358,151 @@ test('a host sheet carrying data-plugin is not mistaken for ours', () => {
|
|
|
304
358
|
assert.equal(results.find(r => r.id === 'token.bgBase').status, 'pass')
|
|
305
359
|
})
|
|
306
360
|
|
|
361
|
+
test('a closed dock holding only its empty seat is n/a, not a failure', () => {
|
|
362
|
+
// The state a healthy host sits in most of the time: the dock holds no tab, so
|
|
363
|
+
// `data-dockkit-content` (a JS-set attribute the host never styles) cannot be
|
|
364
|
+
// observed at all. It used to read as "no host stylesheet declares it" 鈥?a
|
|
365
|
+
// perfectly healthy host reported as broken.
|
|
366
|
+
const env = healthyEnv(DOCK_EMPTY)
|
|
367
|
+
const results = probeContracts(env, 'en')
|
|
368
|
+
const byId = id => results.find(r => r.id === id)
|
|
369
|
+
assert.equal(byId('dockkit.content').status, 'skip')
|
|
370
|
+
// The empty seat IS the thing on screen here, so its own contract answers — it
|
|
371
|
+
// is present (the host renders it visibly in this state; it is only parked and
|
|
372
|
+
// hidden once a tab opens).
|
|
373
|
+
assert.equal(byId('dockkit.empty').status, 'pass')
|
|
374
|
+
assert.equal(byId('dockkit.float').status, 'skip')
|
|
375
|
+
assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
test('a populated dock reports its empty seat as n/a, not as missing', () => {
|
|
379
|
+
// The state the panel showed the user a red line for. With a tab open the host
|
|
380
|
+
// renders no seat on that screen at all — the seat is not the thing being shown —
|
|
381
|
+
// so the question has no answer there, which is n/a and must never be a failure.
|
|
382
|
+
const env = hostEnv({
|
|
383
|
+
ctx: healthyCtx(),
|
|
384
|
+
selectors: [...CORE_PRESENT, 'div[data-sidebar-right-panel]', '[data-dockkit-tab]', '[data-dockkit-content]'],
|
|
385
|
+
})
|
|
386
|
+
const results = probeContracts(env, 'en')
|
|
387
|
+
const byId = id => results.find(r => r.id === id)
|
|
388
|
+
assert.equal(byId('dockkit.empty').status, 'skip')
|
|
389
|
+
assert.equal(byId('dockkit.content').status, 'pass')
|
|
390
|
+
assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('a parked empty seat is still a pass, not a failure', () => {
|
|
394
|
+
// Presence is the whole question. The host parks the seat with
|
|
395
|
+
// `visibility:hidden` as tabs come and go, so asking whether it is VISIBLE is
|
|
396
|
+
// asking about a state the host uses both ways — which is how this contract ended
|
|
397
|
+
// up reporting a red line on a healthy dock.
|
|
398
|
+
const env = hostEnv({
|
|
399
|
+
ctx: healthyCtx(),
|
|
400
|
+
selectors: [...CORE_PRESENT, 'div[data-sidebar-right-panel]', '[data-dockkit-add-tab]', '[data-dockkit-empty]'],
|
|
401
|
+
hidden: ['[data-dockkit-empty]'],
|
|
402
|
+
})
|
|
403
|
+
const results = probeContracts(env, 'en')
|
|
404
|
+
assert.equal(results.find(r => r.id === 'dockkit.empty').status, 'pass')
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('a seat that is gone while the dock is empty fails', () => {
|
|
408
|
+
// The real rename, and the one way this contract can fail: the dock is on screen
|
|
409
|
+
// with the host's "add a tab" button — so it holds no tab — and the seat is not
|
|
410
|
+
// there. (The old form tested the opposite state, a populated dock, instead.)
|
|
411
|
+
const env = hostEnv({
|
|
412
|
+
ctx: healthyCtx(),
|
|
413
|
+
selectors: [...CORE_PRESENT, 'div[data-sidebar-right-panel]', '[data-dockkit-add-tab]'],
|
|
414
|
+
})
|
|
415
|
+
const results = probeContracts(env, 'en')
|
|
416
|
+
const row = results.find(r => r.id === 'dockkit.empty')
|
|
417
|
+
assert.equal(row.status, 'fail')
|
|
418
|
+
assert.match(row.reason, /no element matches/)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
test('a surface that is mounted and empty is n/a, not a renamed marker', () => {
|
|
422
|
+
// The hero screen: a session is open, the composer is there, and the host has
|
|
423
|
+
// deliberately rendered no message column because there are no messages. The
|
|
424
|
+
// previous anchor ("the conversation scrollport exists") is true here, so
|
|
425
|
+
// `[data-chat-flow]` was reported as renamed on a healthy, brand-new session.
|
|
426
|
+
const env = hostEnv({
|
|
427
|
+
ctx: healthyCtx(),
|
|
428
|
+
selectors: ['[data-conversation-session]', '[data-conversation-scroll]', '[data-composer-card]',
|
|
429
|
+
'[data-content-phase]', '[data-shell-overlay]', '.dab-root', ...DOCK_EMPTY],
|
|
430
|
+
})
|
|
431
|
+
const results = probeContracts(env, 'en')
|
|
432
|
+
const byId = id => results.find(r => r.id === id)
|
|
433
|
+
assert.equal(byId('chat.flow').status, 'skip')
|
|
434
|
+
// The two markers that ARE expected in that phase still have to answer, or this
|
|
435
|
+
// guard would hide a real rename behind "the chat is not mounted".
|
|
436
|
+
assert.equal(byId('composer.card').status, 'pass')
|
|
437
|
+
assert.equal(byId('conversation.scroll').status, 'pass')
|
|
438
|
+
assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
test('the trajectory tab is n/a for the chat column, not a failure', () => {
|
|
442
|
+
// Measured on the running host: the trajectory view unmounts the chat column
|
|
443
|
+
// (`data-trajectory-scroll` in, `data-chat-flow` out) while the session root
|
|
444
|
+
// changes phase rather than disappearing. Switching to it used to report
|
|
445
|
+
// "no element matches [data-chat-flow] while its surface is on screen".
|
|
446
|
+
const env = hostEnv({
|
|
447
|
+
ctx: healthyCtx(),
|
|
448
|
+
selectors: ['[data-conversation-session]', '[data-content-phase]', '[data-conversation-scroll]',
|
|
449
|
+
'[data-composer-card]', '[data-conversation-composer-overlay]', '[data-shell-overlay]', '.dab-root',
|
|
450
|
+
...DOCK_POPULATED],
|
|
451
|
+
})
|
|
452
|
+
const results = probeContracts(env, 'en')
|
|
453
|
+
const byId = id => results.find(r => r.id === id)
|
|
454
|
+
assert.equal(byId('chat.flow').status, 'skip')
|
|
455
|
+
assert.equal(byId('trajectory.root').status, 'pass')
|
|
456
|
+
assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
test('a message column that is present but hidden is a failure', () => {
|
|
460
|
+
// The other half of the fix: `visible` must not soften the check into "it is
|
|
461
|
+
// somewhere in the DOM". A column the user cannot see is not a column the card
|
|
462
|
+
// can be drawn on, and the report has to say which of the two happened.
|
|
463
|
+
const env = hostEnv({
|
|
464
|
+
ctx: healthyCtx(),
|
|
465
|
+
selectors: [...CORE_PRESENT, ...DOCK_POPULATED],
|
|
466
|
+
hidden: ['[data-chat-flow]'],
|
|
467
|
+
})
|
|
468
|
+
const results = probeContracts(env, 'en')
|
|
469
|
+
const row = results.find(r => r.id === 'chat.flow')
|
|
470
|
+
assert.equal(row.status, 'fail')
|
|
471
|
+
assert.match(row.reason, /hidden/)
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
test('a renamed dock tab attribute still fails once a tab is open', () => {
|
|
475
|
+
// The guard must not swallow a real rename: with a tab open the host has to
|
|
476
|
+
// carry `data-dockkit-content` on the tab host, and if the name moved, say so.
|
|
477
|
+
// A populated tab is on screen (the host marks the tab chip), but the attribute
|
|
478
|
+
// the contract names no longer matches anything.
|
|
479
|
+
const env = hostEnv({
|
|
480
|
+
ctx: healthyCtx(),
|
|
481
|
+
selectors: ['div[data-sidebar-right-panel]', '[data-dockkit-tab]'],
|
|
482
|
+
})
|
|
483
|
+
const results = probeContracts(env, 'en')
|
|
484
|
+
const row = results.find(r => r.id === 'dockkit.content')
|
|
485
|
+
assert.equal(row.status, 'fail')
|
|
486
|
+
assert.match(row.reason, /data-dockkit-content/)
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
test('a floating pane missing its flag is a failure while it is on screen', () => {
|
|
490
|
+
// The float flag is a JS attribute too, so it is only observable when the user
|
|
491
|
+
// has a pane floated 鈥?and then, if the host stopped setting it, the plugin
|
|
492
|
+
// would paint that pane like a docked one.
|
|
493
|
+
const env = hostEnv({
|
|
494
|
+
ctx: healthyCtx(),
|
|
495
|
+
selectors: [...CORE_PRESENT, ...DOCK_POPULATED, '[data-sidebar-right-mode="float"]'],
|
|
496
|
+
})
|
|
497
|
+
const results = probeContracts(env, 'en')
|
|
498
|
+
const row = results.find(r => r.id === 'dockkit.float')
|
|
499
|
+
assert.equal(row.status, 'fail')
|
|
500
|
+
assert.match(row.reason, /data-dockkit-float/)
|
|
501
|
+
})
|
|
502
|
+
|
|
307
503
|
test('a surface that is merely off screen is n/a, not a failure', () => {
|
|
308
504
|
// The app frame and every service are up (the settings section is rendered
|
|
309
|
-
// inside the frame, so the frame is always there)
|
|
505
|
+
// inside the frame, so the frame is always there) 鈥?but the conversation view,
|
|
310
506
|
// the right panel and the Cordis panel are not. Nothing is broken; those
|
|
311
507
|
// surfaces simply are not there to observe.
|
|
312
508
|
const env = hostEnv({
|
|
@@ -326,7 +522,7 @@ test('a surface that is merely off screen is n/a, not a failure', () => {
|
|
|
326
522
|
})
|
|
327
523
|
|
|
328
524
|
test('a mounted but marker-less surface is a failure, not n/a', () => {
|
|
329
|
-
// The dialog is open, so the right panel's anchor is off screen
|
|
525
|
+
// The dialog is open, so the right panel's anchor is off screen 鈥?but the
|
|
330
526
|
// dialog's own selector still matches. A surface whose anchor IS on screen and
|
|
331
527
|
// whose selector is not is a rename, and must not read as "not observable".
|
|
332
528
|
const env = hostEnv({
|
|
@@ -342,7 +538,7 @@ test('a mounted but marker-less surface is a failure, not n/a', () => {
|
|
|
342
538
|
assert.equal(byId('chat.flow').status, 'skip')
|
|
343
539
|
})
|
|
344
540
|
|
|
345
|
-
//
|
|
541
|
+
// 鈹€鈹€ 3. the settings UI's own stylesheet 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
346
542
|
|
|
347
543
|
test('every dab-* class a component uses is defined in the UI stylesheet', async () => {
|
|
348
544
|
const dir = join(pluginDir, 'src', 'client')
|
|
@@ -372,7 +568,7 @@ test('every dab-* class a component uses is defined in the UI stylesheet', async
|
|
|
372
568
|
assert.deepEqual(missing, [], `classes used but never styled: ${missing.map(m => `${m.cls} (${m.file})`).join(', ')}`)
|
|
373
569
|
})
|
|
374
570
|
|
|
375
|
-
//
|
|
571
|
+
// 鈹€鈹€ 4. the floor comparison 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
376
572
|
|
|
377
573
|
test('the floor comparison is coarse on purpose and exact on the triple', () => {
|
|
378
574
|
assert.equal(meetsFloor('0.1.7-rc.2', DSH_FLOOR), true, 'the floor itself satisfies the floor')
|