dsh-background-by-model 0.4.5 → 0.5.0
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 +6 -20
- package/README.zh.md +6 -20
- package/lib/client.js +2 -1665
- package/lib/client.js.map +1 -1
- package/lib/index.js +0 -24
- package/package.json +4 -9
- package/lib/host-scan-C6rdxkf8.js +0 -1186
- package/lib/host-scan.js +0 -2
- package/lib/testing.js +0 -632
- package/scripts/host-contract-scan.mjs +0 -182
- package/test/host-check.test.mjs +0 -581
package/test/host-check.test.mjs
DELETED
|
@@ -1,581 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The host self-check's own tests 鈥?run with
|
|
3
|
-
*
|
|
4
|
-
* pnpm test
|
|
5
|
-
*
|
|
6
|
-
* Three things are covered, and they are the three that can rot silently:
|
|
7
|
-
*
|
|
8
|
-
* 1. **The contract table holds together** 鈥?unique ids, machine-checkable
|
|
9
|
-
* sources, a known criticality, and at least one check or an explicit
|
|
10
|
-
* "documentation only" marker. A contract point added without a source would
|
|
11
|
-
* otherwise be invisible to the offline scan.
|
|
12
|
-
* 2. **The probe reports what is actually there** 鈥?the whole probe is driven
|
|
13
|
-
* against a fixture written in the shape of a host DOM, so "the host renamed
|
|
14
|
-
* `--dsw-specific-menu`" produces exactly the failure the panel claims, and a
|
|
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** 鈥?every `dab-*`
|
|
17
|
-
* class a component uses is defined in it, and the floor comparison is exact
|
|
18
|
-
* on the release triple.
|
|
19
|
-
*
|
|
20
|
-
* The tests run against the BUILT node entries (`lib/testing.js`), not the
|
|
21
|
-
* TypeScript sources: `node --test` cannot resolve extensionless relative imports
|
|
22
|
-
* inside a `.ts` file, and asserting on the shipped bundle is the more honest
|
|
23
|
-
* target anyway. `pnpm test` bundles first.
|
|
24
|
-
*/
|
|
25
|
-
import { test } from 'node:test'
|
|
26
|
-
import assert from 'node:assert/strict'
|
|
27
|
-
import { dirname, join, resolve } from 'node:path'
|
|
28
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
29
|
-
|
|
30
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
31
|
-
const pluginDir = resolve(here, '..')
|
|
32
|
-
const testingPath = join(pluginDir, 'lib', 'testing.js')
|
|
33
|
-
|
|
34
|
-
const { HOST_CONTRACTS, DSH_FLOOR, probeContracts, verdictOf, meetsFloor, UI_CSS, OWN_SHEET_ATTR, OWN_SHEET_DATASET } =
|
|
35
|
-
await import(pathToFileURL(testingPath).href)
|
|
36
|
-
|
|
37
|
-
const { readFile, readdir } = await import('node:fs/promises')
|
|
38
|
-
|
|
39
|
-
// 鈹€鈹€ 1. the contract table 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
40
|
-
|
|
41
|
-
test('contract ids are unique and non-empty', () => {
|
|
42
|
-
const seen = new Set()
|
|
43
|
-
for (const c of HOST_CONTRACTS) {
|
|
44
|
-
assert.ok(c.id.length > 0, 'a contract has an empty id')
|
|
45
|
-
assert.ok(!seen.has(c.id), `duplicate contract id ${c.id}`)
|
|
46
|
-
seen.add(c.id)
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test('every contract is machine-checkable', () => {
|
|
51
|
-
for (const c of HOST_CONTRACTS) {
|
|
52
|
-
assert.ok(c.target.length > 0, `${c.id} has no target`)
|
|
53
|
-
assert.ok(c.sources.length > 0, `${c.id} declares no source, so the offline scan cannot see it`)
|
|
54
|
-
assert.ok(c.usedBy.length > 0, `${c.id} does not say which plugin file reads it`)
|
|
55
|
-
assert.ok(['core', 'looks', 'label'].includes(c.criticality), `${c.id} has an unknown criticality`)
|
|
56
|
-
assert.ok(['<=0.1.6', '0.1.7'].includes(c.since), `${c.id} has an unknown "since"`)
|
|
57
|
-
for (const s of c.sources) {
|
|
58
|
-
assert.ok(s.literal.length > 0, `${c.id} has a source without a literal`)
|
|
59
|
-
assert.ok(!s.path.includes('\\'), `${c.id} source path must use forward slashes: ${s.path}`)
|
|
60
|
-
if (s.path === '.') assert.equal(s.root, 'frontend', `${c.id}: "." only makes sense for the frontend root`)
|
|
61
|
-
}
|
|
62
|
-
if (c.checks.length === 0) {
|
|
63
|
-
// Documentation-only entries are allowed (a legacy alias we still accept),
|
|
64
|
-
// but they must be the minority and must not claim to be checkable.
|
|
65
|
-
assert.ok(c.criticality !== 'core', `${c.id} is critical but has no check`)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
test('critical contracts carry a check that can fail', () => {
|
|
71
|
-
const critical = HOST_CONTRACTS.filter(c => c.criticality === 'core')
|
|
72
|
-
assert.ok(critical.length >= 4, 'the model hop and the menu split should be critical')
|
|
73
|
-
for (const c of critical) {
|
|
74
|
-
assert.ok(c.checks.length > 0, `${c.id} is critical but nothing verifies it at runtime`)
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
// 鈹€鈹€ 2. the probe, against a fixture in the shape of a host DOM 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
79
|
-
|
|
80
|
-
/** A DOM stand-in with just enough selector support for the contract checks. */
|
|
81
|
-
function makeDom(host) {
|
|
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
|
|
94
|
-
return false
|
|
95
|
-
}
|
|
96
|
-
const matches = sel => sel.split(',').map(p => p.trim()).some(
|
|
97
|
-
part => part.split(/\s+/).every(one),
|
|
98
|
-
)
|
|
99
|
-
return {
|
|
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),
|
|
105
|
-
// Where the host publishes its palette: `body` in the real client, and the
|
|
106
|
-
// fixture models exactly that. Reading the document element instead is what
|
|
107
|
-
// made seven healthy tokens report as empty.
|
|
108
|
-
tokenRoot: () => ({ style: {} }),
|
|
109
|
-
// `on` is null for an unscoped token check (the probe reads it off the token
|
|
110
|
-
// root) and the resolved element otherwise. The fixture answers from its own
|
|
111
|
-
// table either way 鈥?whether a value is readable is the host's own business,
|
|
112
|
-
// and that is exactly what the table stands in for.
|
|
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}`),
|
|
118
|
-
styleSheets: () => [],
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Selectors for the surfaces that are on screen in every fixture. */
|
|
123
|
-
const CORE_PRESENT = [
|
|
124
|
-
'div[data-sidebar-right-panel]',
|
|
125
|
-
'div[role="dialog"][aria-modal="true"][aria-labelledby]',
|
|
126
|
-
'[data-shell-overlay]',
|
|
127
|
-
'[data-composer-card]',
|
|
128
|
-
'[data-cordis-panel]',
|
|
129
|
-
'[data-chat-flow]',
|
|
130
|
-
'[data-chat-turn]',
|
|
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]',
|
|
135
|
-
'[data-conversation-composer-overlay]',
|
|
136
|
-
'.dab-root',
|
|
137
|
-
'[data-dockkit-strip]',
|
|
138
|
-
]
|
|
139
|
-
|
|
140
|
-
/** Every design token the contracts read; each one resolves in a healthy host. */
|
|
141
|
-
const PRESENT_TOKENS = [
|
|
142
|
-
'--dsw-alias-bg-base',
|
|
143
|
-
'--dsw-alias-bg-layer-1',
|
|
144
|
-
'--dsw-alias-bg-layer-2',
|
|
145
|
-
'--dsw-alias-bg-layer-3',
|
|
146
|
-
'--dsw-specific-menu',
|
|
147
|
-
'--dsw-menu-surface-fill',
|
|
148
|
-
'--dsw-specific-sidebar-fill',
|
|
149
|
-
'--dsw-specific-input-major',
|
|
150
|
-
'--dsw-alias-label-caption',
|
|
151
|
-
]
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* The literals the HOST's stylesheets declare. The two halves of a token
|
|
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.
|
|
157
|
-
*/
|
|
158
|
-
const HOST_RULES = [
|
|
159
|
-
'--dsw-menu-surface-fill', '--dsw-specific-menu', '--dsw-specific-sidebar-fill',
|
|
160
|
-
'--dsw-specific-input-major', '--dsw-alias-bg-base', '--dsw-alias-bg-layer-1',
|
|
161
|
-
'--dsw-alias-label-caption',
|
|
162
|
-
'.md-table-wide', '[data-dockkit-content]', '[data-dockkit-empty]',
|
|
163
|
-
'[data-dockkit-float]', '[data-sidebar-right-open]', 'data-ds-dark-theme',
|
|
164
|
-
]
|
|
165
|
-
|
|
166
|
-
/** The one literal only THIS plugin's stylesheet declares (`.dab-card`, `side: 'own'`). */
|
|
167
|
-
const OWN_RULES = ['.dab-card']
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* A stylesheet the probe may read.
|
|
171
|
-
*
|
|
172
|
-
* `own: true` writes this plugin's marker 鈥?the ONLY thing that tells our sheets
|
|
173
|
-
* apart from the host's, since the host's own CSS modules set `data-plugin` on
|
|
174
|
-
* theirs, so a probe keying off that skips the entire host theme.
|
|
175
|
-
*
|
|
176
|
-
* The fixture exposes the attribute the way a REAL element does: `getAttribute`
|
|
177
|
-
* answers the dashed name, and `dataset` only the camel-cased key. Modelling this
|
|
178
|
-
* wrong is how a probe that reads `dataset['data-dab-side']` passes here and
|
|
179
|
-
* silently fails in a browser.
|
|
180
|
-
*/
|
|
181
|
-
function sheet(literals, own = false) {
|
|
182
|
-
const attrs = {}
|
|
183
|
-
const dataset = {}
|
|
184
|
-
if (own) {
|
|
185
|
-
attrs[OWN_SHEET_ATTR] = 'own'
|
|
186
|
-
dataset[OWN_SHEET_DATASET] = 'own'
|
|
187
|
-
}
|
|
188
|
-
return {
|
|
189
|
-
ownerNode: {
|
|
190
|
-
dataset,
|
|
191
|
-
getAttribute: name => (name in attrs ? attrs[name] : null),
|
|
192
|
-
setAttribute: (name, value) => { attrs[name] = value },
|
|
193
|
-
},
|
|
194
|
-
cssRules: literals.map(l => ({ cssText: `.x{color:red}/* ${l} */` })),
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** The host's sheets plus our own, minus whatever a test removes. */
|
|
199
|
-
function hostStyles(literals = HOST_RULES, ownLiterals = OWN_RULES) {
|
|
200
|
-
const sheets = [sheet(literals)]
|
|
201
|
-
if (ownLiterals.length > 0) sheets.push(sheet(ownLiterals, true))
|
|
202
|
-
return sheets
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* A fixture host: `selectors` are the ones that match, `rules` the literals the
|
|
207
|
-
* host stylesheet declares, and every token in `PRESENT_TOKENS` resolves unless
|
|
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.
|
|
210
|
-
*/
|
|
211
|
-
function hostEnv({ ctx, selectors = CORE_PRESENT, rules = HOST_RULES, tokens = PRESENT_TOKENS, hidden = [] }) {
|
|
212
|
-
// A Map, not a Set: the fixture must answer with the token's VALUE, and only a
|
|
213
|
-
// Map carries one (a Set has no `get`).
|
|
214
|
-
const host = new Map()
|
|
215
|
-
for (const s of selectors) host.set(s, true)
|
|
216
|
-
for (const s of hidden) host.set(`hidden:${s}`, true)
|
|
217
|
-
for (const t of tokens) host.set(`computed:${t}`, '#123456')
|
|
218
|
-
return { ...makeDom(host), styleSheets: () => hostStyles(rules), ctx }
|
|
219
|
-
}
|
|
220
|
-
|
|
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
|
-
})
|
|
252
|
-
|
|
253
|
-
/** A context publishing the four services the model hop needs. */
|
|
254
|
-
function healthyCtx(overrides = {}) {
|
|
255
|
-
const face = { subscribe: () => () => {}, getSnapshot: () => ({}) }
|
|
256
|
-
const services = {
|
|
257
|
-
sessions: { list: face, binding: () => ({}) },
|
|
258
|
-
uiSession: { current: face },
|
|
259
|
-
modelDirectories: { directoryFor: () => ({ store: face }) },
|
|
260
|
-
...overrides,
|
|
261
|
-
}
|
|
262
|
-
return { get: id => services[id] }
|
|
263
|
-
}
|
|
264
|
-
|
|
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
|
-
}
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
test('a renamed menu token fails exactly that contract, and names the symptom', () => {
|
|
276
|
-
// The 0.1.8 rename: the host stops declaring the token the sticky group
|
|
277
|
-
// headings paint from. Everything else still answers.
|
|
278
|
-
const env = hostEnv({ ctx: healthyCtx(), rules: HOST_RULES.filter(l => l !== '--dsw-menu-surface-fill') })
|
|
279
|
-
const results = probeContracts(env, 'en')
|
|
280
|
-
const failed = results.filter(r => r.status === 'fail').map(r => r.id)
|
|
281
|
-
assert.deepEqual(failed, ['token.menuSurface'])
|
|
282
|
-
const row = results.find(r => r.id === 'token.menuSurface')
|
|
283
|
-
assert.match(row.reason, /--dsw-menu-surface-fill/)
|
|
284
|
-
assert.match(row.symptom, /solid band|white band|sticky/i)
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
test('a renamed token the host still aliases is caught by the rule half', () => {
|
|
288
|
-
// The 0.1.7 menu split in reverse: the host keeps the OLD name working (aliased)
|
|
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
|
|
291
|
-
// tell "renamed" from "still there".
|
|
292
|
-
const env = hostEnv({
|
|
293
|
-
ctx: healthyCtx(),
|
|
294
|
-
rules: HOST_RULES.filter(l => l !== '--dsw-menu-surface-fill'),
|
|
295
|
-
})
|
|
296
|
-
const results = probeContracts(env, 'en')
|
|
297
|
-
const row = results.find(r => r.id === 'token.menuSurface')
|
|
298
|
-
assert.equal(row.status, 'fail')
|
|
299
|
-
// The computed half would have passed: the token still resolves a value.
|
|
300
|
-
assert.match(row.detail, /host stylesheet includes/)
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
test('a host that stops publishing the palette fails the computed half', () => {
|
|
304
|
-
// The other failure shape: the name is still declared somewhere, but body no
|
|
305
|
-
// longer carries a value 鈥?the case that made 7 healthy tokens look empty when
|
|
306
|
-
// the probe read the wrong element.
|
|
307
|
-
const env = hostEnv({
|
|
308
|
-
ctx: healthyCtx(),
|
|
309
|
-
tokens: PRESENT_TOKENS.filter(t => t !== '--dsw-alias-bg-base'),
|
|
310
|
-
})
|
|
311
|
-
const results = probeContracts(env, 'en')
|
|
312
|
-
const row = results.find(r => r.id === 'token.bgBase')
|
|
313
|
-
assert.equal(row.status, 'fail')
|
|
314
|
-
assert.match(row.reason, /--dsw-alias-bg-base/)
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
test('a dropped session service fails the model hop', () => {
|
|
318
|
-
const env = hostEnv({ ctx: healthyCtx({ sessions: undefined }) })
|
|
319
|
-
const results = probeContracts(env, 'en')
|
|
320
|
-
assert.equal(results.find(r => r.id === 'sessions.list').status, 'fail')
|
|
321
|
-
assert.equal(results.find(r => r.id === 'sessions.binding.projections').status, 'skip')
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
test('the plugin\'s own token stylesheet cannot make a host check pass', () => {
|
|
325
|
-
// The situation that produced the 0.1.7 white band: the plugin re-emits the
|
|
326
|
-
// menu fill itself, so its own sheet declares the token. That re-emission must
|
|
327
|
-
// never be accepted as evidence about the HOST 鈥?otherwise the check is green
|
|
328
|
-
// by construction on exactly the host where the token was renamed away.
|
|
329
|
-
const env = hostEnv({
|
|
330
|
-
ctx: healthyCtx(),
|
|
331
|
-
// The host sheet is readable but has moved on: it no longer declares the
|
|
332
|
-
// menu fill under this name.
|
|
333
|
-
rules: [],
|
|
334
|
-
})
|
|
335
|
-
env.styleSheets = () => [
|
|
336
|
-
sheet([]), // the host's own, readable, declaring nothing
|
|
337
|
-
sheet(['--dsw-menu-surface-fill', '--dsw-specific-menu', '.dab-card'], true),
|
|
338
|
-
]
|
|
339
|
-
const results = probeContracts(env, 'en')
|
|
340
|
-
const byId = id => results.find(r => r.id === id)
|
|
341
|
-
assert.equal(byId('token.menuSurface').status, 'fail')
|
|
342
|
-
assert.match(byId('token.menuSurface').reason, /no host stylesheet declares/)
|
|
343
|
-
assert.equal(byId('token.menuAlias').status, 'fail')
|
|
344
|
-
// 鈥hile the contract whose other half IS our own sheet still passes: this is
|
|
345
|
-
// what `side: 'own'` is for, and why the two sides must never be merged.
|
|
346
|
-
assert.equal(byId('settings.card').status, 'pass')
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
test('a host sheet carrying data-plugin is not mistaken for ours', () => {
|
|
350
|
-
// Regression for the probe bug that reported 7 healthy tokens as renamed: the
|
|
351
|
-
// host's CSS modules ALSO set `data-plugin` on their <style> elements, so a
|
|
352
|
-
// probe that skips "sheets with data-plugin" skips the host's whole theme.
|
|
353
|
-
const hostSheet = sheet(['--dsw-alias-bg-base'])
|
|
354
|
-
hostSheet.ownerNode.dataset.plugin = '@deepseek-ai/dsh-client-ui-theme'
|
|
355
|
-
const env = hostEnv({ ctx: healthyCtx(), rules: [] })
|
|
356
|
-
env.styleSheets = () => [hostSheet]
|
|
357
|
-
const results = probeContracts(env, 'en')
|
|
358
|
-
assert.equal(results.find(r => r.id === 'token.bgBase').status, 'pass')
|
|
359
|
-
})
|
|
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
|
-
|
|
503
|
-
test('a surface that is merely off screen is n/a, not a failure', () => {
|
|
504
|
-
// The app frame and every service are up (the settings section is rendered
|
|
505
|
-
// inside the frame, so the frame is always there) 鈥?but the conversation view,
|
|
506
|
-
// the right panel and the Cordis panel are not. Nothing is broken; those
|
|
507
|
-
// surfaces simply are not there to observe.
|
|
508
|
-
const env = hostEnv({
|
|
509
|
-
ctx: healthyCtx(),
|
|
510
|
-
selectors: ['.dab-root', 'div[role="dialog"][aria-modal="true"][aria-labelledby]', '[data-shell-overlay]'],
|
|
511
|
-
})
|
|
512
|
-
const results = probeContracts(env, 'en')
|
|
513
|
-
const byId = id => results.find(r => r.id === id)
|
|
514
|
-
assert.equal(byId('chat.flow').status, 'skip')
|
|
515
|
-
assert.equal(byId('conversation.scroll').status, 'skip')
|
|
516
|
-
assert.equal(byId('trajectory.root').status, 'skip')
|
|
517
|
-
assert.equal(byId('rightbar.panel').status, 'skip')
|
|
518
|
-
assert.equal(byId('cordis.panel').status, 'skip')
|
|
519
|
-
assert.equal(byId('composer.card').status, 'skip')
|
|
520
|
-
assert.equal(byId('settings.dialog').status, 'pass')
|
|
521
|
-
assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
test('a mounted but marker-less surface is a failure, not n/a', () => {
|
|
525
|
-
// The dialog is open, so the right panel's anchor is off screen 鈥?but the
|
|
526
|
-
// dialog's own selector still matches. A surface whose anchor IS on screen and
|
|
527
|
-
// whose selector is not is a rename, and must not read as "not observable".
|
|
528
|
-
const env = hostEnv({
|
|
529
|
-
ctx: healthyCtx(),
|
|
530
|
-
// The dock chrome is mounted (the panel container exists) but the panel
|
|
531
|
-
// attribute the contract names is gone.
|
|
532
|
-
selectors: ['.dab-root', '[data-dockkit-strip]'],
|
|
533
|
-
})
|
|
534
|
-
const results = probeContracts(env, 'en')
|
|
535
|
-
const byId = id => results.find(r => r.id === id)
|
|
536
|
-
assert.equal(byId('rightbar.panel').status, 'fail')
|
|
537
|
-
assert.match(byId('rightbar.panel').reason, /data-sidebar-right-panel/)
|
|
538
|
-
assert.equal(byId('chat.flow').status, 'skip')
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
// 鈹€鈹€ 3. the settings UI's own stylesheet 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
542
|
-
|
|
543
|
-
test('every dab-* class a component uses is defined in the UI stylesheet', async () => {
|
|
544
|
-
const dir = join(pluginDir, 'src', 'client')
|
|
545
|
-
const files = []
|
|
546
|
-
const walk = async d => {
|
|
547
|
-
for (const entry of await readdir(d, { withFileTypes: true })) {
|
|
548
|
-
const p = join(d, entry.name)
|
|
549
|
-
if (entry.isDirectory()) await walk(p)
|
|
550
|
-
else if (entry.name.endsWith('.tsx')) files.push(p)
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
await walk(dir)
|
|
554
|
-
assert.ok(files.length > 0, 'no component sources found')
|
|
555
|
-
|
|
556
|
-
const missing = []
|
|
557
|
-
for (const file of files) {
|
|
558
|
-
const text = await readFile(file, 'utf8')
|
|
559
|
-
for (const m of text.matchAll(/(?<![\w-])dab-[a-z0-9-]+/g)) {
|
|
560
|
-
const cls = m[0]
|
|
561
|
-
// `--dab-mono` and friends are this plugin's own CSS VARIABLES, not classes.
|
|
562
|
-
if (text.slice(Math.max(0, m.index - 2), m.index) === '--') continue
|
|
563
|
-
if (UI_CSS.includes(`.${cls}`)) continue
|
|
564
|
-
if (missing.some(x => x.cls === cls && x.file === file)) continue
|
|
565
|
-
missing.push({ cls, file: file.slice(pluginDir.length + 1) })
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
assert.deepEqual(missing, [], `classes used but never styled: ${missing.map(m => `${m.cls} (${m.file})`).join(', ')}`)
|
|
569
|
-
})
|
|
570
|
-
|
|
571
|
-
// 鈹€鈹€ 4. the floor comparison 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
572
|
-
|
|
573
|
-
test('the floor comparison is coarse on purpose and exact on the triple', () => {
|
|
574
|
-
assert.equal(meetsFloor('0.1.7-rc.2', DSH_FLOOR), true, 'the floor itself satisfies the floor')
|
|
575
|
-
assert.equal(meetsFloor('0.1.7', DSH_FLOOR), true)
|
|
576
|
-
assert.equal(meetsFloor('0.2.0', DSH_FLOOR), true)
|
|
577
|
-
assert.equal(meetsFloor('1.0.0', DSH_FLOOR), true)
|
|
578
|
-
assert.equal(meetsFloor('0.1.6', DSH_FLOOR), false)
|
|
579
|
-
assert.equal(meetsFloor('0.1.0', DSH_FLOOR), false)
|
|
580
|
-
assert.equal(meetsFloor('not-a-version', DSH_FLOOR), null)
|
|
581
|
-
})
|