dsh-dbhub-live 5.1.0 → 5.2.0-dev.3
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/AGENTS.md +116 -254
- package/README.en.md +96 -11
- package/README.md +97 -12
- package/doc/ARCHITECTURE.md +108 -0
- package/doc/CLIENT.md +124 -0
- package/doc/DESENSITIZE.md +56 -0
- package/doc/DEVELOPMENT.md +139 -0
- package/doc/IMPACT-MATRIX.md +36 -0
- package/doc/RELEASE.md +93 -0
- package/doc/REQUIREMENTS.md +41 -3
- package/lib/client.js +704 -27
- package/lib/config.mjs +3 -0
- package/lib/i18n.mjs +14 -4
- package/lib/index.mjs +107 -0
- package/lib/meta.mjs +267 -0
- package/lib/options.mjs +16 -1
- package/lib/policy.mjs +92 -0
- package/lib/tools.mjs +4 -1
- package/lib/update.mjs +244 -0
- package/package.json +32 -9
- package/scripts/desensitize-check.mjs +10 -3
- package/scripts/desensitize.config.mjs +18 -10
- package/scripts/docs-check.mjs +214 -0
- package/scripts/release-preflight.mjs +180 -0
- package/scripts/render-check.mjs +286 -2
- package/test/client-format.test.mjs +89 -5
- package/test/meta.test.mjs +234 -0
- package/test/options.test.mjs +28 -15
- package/test/policy.test.mjs +132 -0
- package/test/update.test.mjs +270 -0
package/scripts/render-check.mjs
CHANGED
|
@@ -148,6 +148,19 @@ const primitivesStub = (() => {
|
|
|
148
148
|
IconCloseOutline16: icon('ico-close'),
|
|
149
149
|
IconTrashOutline16: icon('ico-trash'),
|
|
150
150
|
IconPlayOutline16: icon('ico-run'),
|
|
151
|
+
// The harness's real toggle is a controlled `role=switch` button; the stub
|
|
152
|
+
// mirrors its contract so the gate exercises the primitive path, not just our
|
|
153
|
+
// fallback.
|
|
154
|
+
Switch: (props) => react.createElement('button', {
|
|
155
|
+
type: 'button',
|
|
156
|
+
role: 'switch',
|
|
157
|
+
'aria-checked': props.checked === true,
|
|
158
|
+
'aria-label': props.label,
|
|
159
|
+
disabled: props.disabled === true,
|
|
160
|
+
title: props.title,
|
|
161
|
+
className: 'dsh-switch-stub' + (props.className ? ' ' + props.className : ''),
|
|
162
|
+
onClick: () => { if (typeof props.onChange === 'function') props.onChange(props.checked !== true) },
|
|
163
|
+
}, react.createElement('span', { className: 'dsh-switch-thumb' })),
|
|
151
164
|
writeClipboard: () => Promise.resolve(true),
|
|
152
165
|
}
|
|
153
166
|
})()
|
|
@@ -461,8 +474,9 @@ function page(rows, opts = {}) {
|
|
|
461
474
|
const snapshot = snapshotOf(rows, opts.snapshot)
|
|
462
475
|
const m = mount(snapshot, opts)
|
|
463
476
|
liveMount = m
|
|
464
|
-
const
|
|
465
|
-
|
|
477
|
+
const slot = opts.slot || 'settings.section'
|
|
478
|
+
const entry = m.registrations[slot]
|
|
479
|
+
if (!entry) throw new Error(`${slot} was not registered`)
|
|
466
480
|
const props = Object.assign({}, entry.def.inject(), opts.props, { view: 'page' })
|
|
467
481
|
const comp = entry.comp
|
|
468
482
|
const renderAgain = () => react.render(comp, props)
|
|
@@ -470,6 +484,50 @@ function page(rows, opts = {}) {
|
|
|
470
484
|
return Object.assign(m, { props, comp, renderAgain, tree: react.render(comp, props) })
|
|
471
485
|
}
|
|
472
486
|
|
|
487
|
+
/** The browser viewport width the page sees (the bundle falls back to it when
|
|
488
|
+
* ResizeObserver is unavailable — which is this offscreen harness). */
|
|
489
|
+
function viewport(px) {
|
|
490
|
+
windowShim.innerWidth = px
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** A modern (dsh >= 0.2.1) host view: version fields + an empty update snapshot. */
|
|
494
|
+
const MODERN_DEFAULTS = {
|
|
495
|
+
dshModern: true,
|
|
496
|
+
pluginVersion: '5.2.0-dev.1',
|
|
497
|
+
dshVersion: '0.2.1-rc.2',
|
|
498
|
+
runtime: 'web',
|
|
499
|
+
update: '',
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Set a `.dbh-switch-input` (the modern 显示/隐藏 toggle lives in it). */
|
|
503
|
+
function switchWithin(node, cls, index, checked) {
|
|
504
|
+
const scope = classed(node, cls)[0]
|
|
505
|
+
if (!scope) throw new Error(`no .${cls} in the tree`)
|
|
506
|
+
const inputs = elements(scope, (el) => el.type === 'input'
|
|
507
|
+
&& String(el.props.className || '').split(/\s+/).includes('dbh-switch-input'))
|
|
508
|
+
const input = inputs[index]
|
|
509
|
+
if (!input) throw new Error(`no switch ${index} inside .${cls} (found ${inputs.length})`)
|
|
510
|
+
input.props.onChange({ target: { checked } })
|
|
511
|
+
return input
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Click the nth `role=switch` control inside the first `.cls` scope (the
|
|
515
|
+
* harness Switch primitive's contract: click asks for the opposite state). */
|
|
516
|
+
function clickSwitchWithin(node, cls, index) {
|
|
517
|
+
const scope = classed(node, cls)[0]
|
|
518
|
+
if (!scope) throw new Error(`no .${cls} in the tree`)
|
|
519
|
+
const switches = elements(scope, (el) => el.type === 'button' && el.props.role === 'switch')
|
|
520
|
+
const sw = switches[index]
|
|
521
|
+
if (!sw) throw new Error(`no role=switch ${index} inside .${cls} (found ${switches.length})`)
|
|
522
|
+
sw.props.onClick({ stopPropagation() {}, preventDefault() {} })
|
|
523
|
+
return sw
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** All `role=switch` buttons in the tree. */
|
|
527
|
+
function switches(node) {
|
|
528
|
+
return elements(node, (el) => el.type === 'button' && el.props.role === 'switch')
|
|
529
|
+
}
|
|
530
|
+
|
|
473
531
|
console.log('\n[1] tiled layout (every workspace has exactly one environment)')
|
|
474
532
|
{
|
|
475
533
|
const p = page(TILED, { props: { useWorkspaces: wsSnap('D:/work/app') } })
|
|
@@ -610,6 +668,19 @@ console.log('\n[8] interaction: add panel (global + per group), settings strip,
|
|
|
610
668
|
check('sidebar switch writes showSidebarEntry immediately',
|
|
611
669
|
writes.some(([k, v]) => k === 'showSidebarEntry' && v === false), JSON.stringify(writes))
|
|
612
670
|
check('the settings summary reflects the switch', toHtml(p.tree).includes('侧边栏入口:隐藏'))
|
|
671
|
+
|
|
672
|
+
// The policy announcement is a SECOND, independent switch: it changes what
|
|
673
|
+
// the model sees (one system-prompt section), never what the tools can do.
|
|
674
|
+
check('strip carries the policy announcement switch', toHtml(p.tree).includes('向模型注入数据库访问策略'))
|
|
675
|
+
const beforePolicy = p.setCalls.length
|
|
676
|
+
checkBoxWithin(p.tree, 'dbh-strip', 1, false)
|
|
677
|
+
p.tree = p.renderAgain()
|
|
678
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
679
|
+
const policyWrites = p.setCalls.slice(beforePolicy)
|
|
680
|
+
check('policy switch writes announceAccessPolicy immediately',
|
|
681
|
+
policyWrites.some(([k, v]) => k === 'announceAccessPolicy' && v === false), JSON.stringify(policyWrites))
|
|
682
|
+
check('disabling the announcement does not touch the enabled state',
|
|
683
|
+
!policyWrites.some(([k]) => k === 'enabled'), JSON.stringify(policyWrites))
|
|
613
684
|
}
|
|
614
685
|
|
|
615
686
|
console.log('\n[9] summary view (Plugins page one-liner)')
|
|
@@ -1076,7 +1147,220 @@ console.log('\n[22] a running probe shows its elapsed time and cleans the ticker
|
|
|
1076
1147
|
p.dispose()
|
|
1077
1148
|
}
|
|
1078
1149
|
|
|
1150
|
+
console.log('\n[24] modern wide page: quiet header, DSH-style settings rows, inline read-only chip')
|
|
1151
|
+
{
|
|
1152
|
+
viewport(1440)
|
|
1153
|
+
const p = page(TILED, {
|
|
1154
|
+
slot: 'main',
|
|
1155
|
+
snapshot: Object.assign({}, MODERN_DEFAULTS),
|
|
1156
|
+
props: { useWorkspaces: wsSnap('D:/work/app') },
|
|
1157
|
+
})
|
|
1158
|
+
let html = toHtml(p.tree)
|
|
1159
|
+
check('wide surface class applied', classed(p.tree, 'dbh-wide').length === 1, html.slice(0, 200))
|
|
1160
|
+
check('modern class applied', classed(p.tree, 'dbh-modern').length === 1)
|
|
1161
|
+
check('title bar rendered', count(p.tree, 'dbh-titlebar') === 1)
|
|
1162
|
+
check('title is the plugin name', classed(p.tree, 'dbh-title').map((el) => toHtml(el)).some((t) => t.includes('DBHub 数据库工具')), toHtml(classed(p.tree, 'dbh-title')[0]))
|
|
1163
|
+
check('current version shown next to the title', html.includes('v5.2.0-dev.1'))
|
|
1164
|
+
check('no update UI when already current', classed(p.tree, 'dbh-updatelink').length === 0 && count(p.tree, 'dbh-updatepanel') === 0)
|
|
1165
|
+
check('update check button offered (icon only)', classed(p.tree, 'dbh-vercheck').length === 1)
|
|
1166
|
+
check('status line is state + connection count', count(p.tree, 'dbh-statusline') === 1 && html.includes('运行中') && html.includes('个连接'))
|
|
1167
|
+
check('tool count / one-shot mode no longer in the header', !html.includes('4 个工具') && !html.includes('固定'), html.slice(0, 300))
|
|
1168
|
+
check('no runtime badge in the header', !html.includes('Web 版') && !html.includes('桌面版'))
|
|
1169
|
+
check('settings bar visible (wide page owns it)', count(p.tree, 'dbh-settings') === 1 && count(p.tree, 'dbh-settingsbar') === 1)
|
|
1170
|
+
check('four settings groups on one compact line', count(p.tree, 'dbh-bargroup') === 4, `got ${count(p.tree, 'dbh-bargroup')}`)
|
|
1171
|
+
check('three harness Switch controls (enable / sidebar / policy)', switches(p.tree).length === 3, `got ${switches(p.tree).length}`)
|
|
1172
|
+
check('dbhub update interval stays on the same line', classed(p.tree, 'dbh-srow-input').length === 1)
|
|
1173
|
+
check('no stretched settings rows in wide mode', count(p.tree, 'dbh-srow') === 0)
|
|
1174
|
+
check('no old four-cell grid / gear strip', count(p.tree, 'dbh-config') === 0 && count(p.tree, 'dbh-strip') === 0)
|
|
1175
|
+
check('a read-only chip per row (outside the edit form)', classed(p.tree, 'dbh-rochip').length === 3, `got ${classed(p.tree, 'dbh-rochip').length}`)
|
|
1176
|
+
check('row actions stay icon-only (source + test + edit + delete)', classed(p.tree, 'dbh-iconbtn').length === 8, `got ${classed(p.tree, 'dbh-iconbtn').length}`)
|
|
1177
|
+
// Notes are collapsed: the page acts first; the "why" is one click away.
|
|
1178
|
+
check('technical details start collapsed', count(p.tree, 'dbh-notes') === 0)
|
|
1179
|
+
clickClass(p.tree, 'dbh-linkbtn') // ⓘ 说明与技术详情
|
|
1180
|
+
p.tree = p.renderAgain()
|
|
1181
|
+
html = toHtml(p.tree)
|
|
1182
|
+
check('notes expand with tool count / policy / versions', count(p.tree, 'dbh-notes') === 1 && html.includes('4 个工具') && html.includes('v5.2.0-dev.1'))
|
|
1183
|
+
// Opening the page asks the Host for a THROTTLED update check (op only; the
|
|
1184
|
+
// Host owns the network call and the cache).
|
|
1185
|
+
const checks = p.opCalls.filter((o) => o.op === 'updateCheck')
|
|
1186
|
+
check('page open dispatches a throttled update check', checks.length === 1 && checks[0].force === false, JSON.stringify(p.opCalls))
|
|
1187
|
+
const before = p.setCalls.length
|
|
1188
|
+
clickSwitchWithin(p.tree, 'dbh-settingsbar', 0)
|
|
1189
|
+
check('enable switch disables the plugin', p.setCalls.slice(before).some(([k, v]) => k === 'enabled' && v === false), JSON.stringify(p.setCalls.slice(before)))
|
|
1190
|
+
p.tree = p.renderAgain()
|
|
1191
|
+
const beforeSidebar = p.setCalls.length
|
|
1192
|
+
clickSwitchWithin(p.tree, 'dbh-settingsbar', 1)
|
|
1193
|
+
// saveConfig chains one `host.set` per field through a promise, so the write
|
|
1194
|
+
// lands on the next microtask (the immediate-apply contract, not a sync call).
|
|
1195
|
+
await Promise.resolve()
|
|
1196
|
+
await Promise.resolve()
|
|
1197
|
+
check('sidebar switch saves 显示/隐藏 preference', p.setCalls.slice(beforeSidebar).some(([k, v]) => k === 'showSidebarEntry' && v === false), JSON.stringify(p.setCalls.slice(beforeSidebar)))
|
|
1198
|
+
// The row chip is an options-only op: never a DSN.
|
|
1199
|
+
const beforeOps = p.opCalls.length
|
|
1200
|
+
clickClass(p.tree, 'dbh-rochip')
|
|
1201
|
+
const roOp = p.opCalls.slice(beforeOps).find((o) => o.op === 'options')
|
|
1202
|
+
check('row read-only chip writes {op:options, readOnly:true}', !!roOp && roOp.readOnly === true, JSON.stringify(roOp))
|
|
1203
|
+
check('row read-only chip carries no dsn', !!roOp && roOp.dsn === undefined, JSON.stringify(roOp))
|
|
1204
|
+
p.dispose()
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
console.log('\n[25] modern update link: quiet by default, neutral detail panel on click')
|
|
1208
|
+
{
|
|
1209
|
+
const update = JSON.stringify({
|
|
1210
|
+
latest: '5.2.0', latestSpec: 'dsh-dbhub-live@5.2.0', hasUpdate: true,
|
|
1211
|
+
checkedAt: Date.now(), source: 'npm', error: '',
|
|
1212
|
+
})
|
|
1213
|
+
viewport(1440)
|
|
1214
|
+
const p = page(TILED, {
|
|
1215
|
+
slot: 'main',
|
|
1216
|
+
snapshot: Object.assign({}, MODERN_DEFAULTS, { update }),
|
|
1217
|
+
props: { useWorkspaces: wsSnap('D:/work/app') },
|
|
1218
|
+
})
|
|
1219
|
+
let html = toHtml(p.tree)
|
|
1220
|
+
check('update surfaces as a quiet text link', classed(p.tree, 'dbh-updatelink').length === 1 && html.includes('可升级到 5.2.0'), html.slice(0, 300))
|
|
1221
|
+
check('no red/error-styled update card exists', classed(p.tree, 'dbh-latest').length === 0 && count(p.tree, 'dbh-updatecard') === 0)
|
|
1222
|
+
check('details stay collapsed until asked for', count(p.tree, 'dbh-updatepanel') === 0)
|
|
1223
|
+
clickClass(p.tree, 'dbh-updatelink')
|
|
1224
|
+
p.tree = p.renderAgain()
|
|
1225
|
+
html = toHtml(p.tree)
|
|
1226
|
+
check('clicking the link opens the neutral detail panel', count(p.tree, 'dbh-updatepanel') === 1)
|
|
1227
|
+
check('panel carries the install spec', html.includes('dsh-dbhub-live@5.2.0'))
|
|
1228
|
+
check('web runtime gets the web update command', html.includes('dsh plugin --profile web up dsh-dbhub-live@5.2.0'))
|
|
1229
|
+
const releaseHref = elements(p.tree, (el) => typeof el.props.href === 'string').map((el) => el.props.href)
|
|
1230
|
+
check('release notes link points at the GitHub release', releaseHref.some((h) => h.includes('https://github.com/mr-mihu/dsh-dbhub-live/releases/tag/v5.2.0')), JSON.stringify(releaseHref))
|
|
1231
|
+
// The copy control copies the exact install spec (the thing a person pastes).
|
|
1232
|
+
let copied = ''
|
|
1233
|
+
const previousClipboard = primitivesStub.writeClipboard
|
|
1234
|
+
primitivesStub.writeClipboard = (text) => { copied = String(text); return Promise.resolve(true) }
|
|
1235
|
+
clickClass(p.tree, 'dbh-linkbtn')
|
|
1236
|
+
primitivesStub.writeClipboard = previousClipboard
|
|
1237
|
+
check('copy control copies dsh-dbhub-live@<latest>', copied === 'dsh-dbhub-live@5.2.0', JSON.stringify(copied))
|
|
1238
|
+
// The explicit check button forces a refresh (bypassing the Host throttle).
|
|
1239
|
+
const before = p.opCalls.length
|
|
1240
|
+
clickClass(p.tree, 'dbh-vercheck')
|
|
1241
|
+
check('check button forces a fresh check', p.opCalls.slice(before).some((o) => o.op === 'updateCheck' && o.force === true), JSON.stringify(p.opCalls.slice(before)))
|
|
1242
|
+
p.dispose()
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
console.log('\n[26] modern update panel on desktop: uninstall/reinstall guidance, no web command')
|
|
1246
|
+
{
|
|
1247
|
+
viewport(1440)
|
|
1248
|
+
const update = JSON.stringify({
|
|
1249
|
+
latest: '5.2.0', latestSpec: 'dsh-dbhub-live@5.2.0', hasUpdate: true,
|
|
1250
|
+
checkedAt: Date.now(), source: 'npm', error: '',
|
|
1251
|
+
})
|
|
1252
|
+
const p = page(TILED, {
|
|
1253
|
+
slot: 'main',
|
|
1254
|
+
snapshot: Object.assign({}, MODERN_DEFAULTS, { runtime: 'desktop', update }),
|
|
1255
|
+
props: { useWorkspaces: wsSnap('D:/work/app') },
|
|
1256
|
+
})
|
|
1257
|
+
let html = toHtml(p.tree)
|
|
1258
|
+
check('no desktop runtime badge in the header', !html.includes('桌面版'))
|
|
1259
|
+
clickClass(p.tree, 'dbh-updatelink')
|
|
1260
|
+
p.tree = p.renderAgain()
|
|
1261
|
+
html = toHtml(p.tree)
|
|
1262
|
+
check('desktop guidance says uninstall + reinstall', html.includes('卸载') && html.includes('重新安装'))
|
|
1263
|
+
check('desktop guidance keeps the data location', html.includes('~/.dsh/storages/dsh-dbhub-live'))
|
|
1264
|
+
check('desktop panel offers no web install command', !html.includes('dsh plugin --profile web up'), html.slice(0, 400))
|
|
1265
|
+
p.dispose()
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
console.log('\n[27] modern settings dialog: compact header, 设置 disclosure, read-only chips visible')
|
|
1269
|
+
{
|
|
1270
|
+
viewport(1440)
|
|
1271
|
+
const p = page(TILED, {
|
|
1272
|
+
snapshot: Object.assign({}, MODERN_DEFAULTS),
|
|
1273
|
+
props: { useWorkspaces: wsSnap('D:/work/app') },
|
|
1274
|
+
})
|
|
1275
|
+
let html = toHtml(p.tree)
|
|
1276
|
+
check('modal surface class applied', classed(p.tree, 'dbh-modal').length === 1)
|
|
1277
|
+
check('modal is not the wide page', classed(p.tree, 'dbh-wide').length === 0)
|
|
1278
|
+
check('modal shows the title + version header', count(p.tree, 'dbh-titlebar') === 1 && html.includes('v5.2.0-dev.1'))
|
|
1279
|
+
check('no legacy header or gear strip in the modern dialog', count(p.tree, 'dbh-head') === 0 && count(p.tree, 'dbh-strip') === 0 && !html.includes('⚙'))
|
|
1280
|
+
check('settings sit behind the status-line toggle (not in the header)', html.includes('设置') && classed(p.tree, 'dbh-linkbtn').length >= 1)
|
|
1281
|
+
check('read-only chip is available in the dialog too', classed(p.tree, 'dbh-rochip').length === 3, `got ${classed(p.tree, 'dbh-rochip').length}`)
|
|
1282
|
+
// uiMemory survives mounts, so an earlier group may have left the disclosure
|
|
1283
|
+
// open; either way the toggle must own the rows and close them on the next click.
|
|
1284
|
+
if (count(p.tree, 'dbh-settings') === 0) {
|
|
1285
|
+
clickClass(p.tree, 'dbh-linkbtn')
|
|
1286
|
+
p.tree = p.renderAgain()
|
|
1287
|
+
}
|
|
1288
|
+
html = toHtml(p.tree)
|
|
1289
|
+
check('the dialog opens the same DSH-style rows', count(p.tree, 'dbh-settings') === 1 && count(p.tree, 'dbh-srow') === 4)
|
|
1290
|
+
check('sidebar/policy controls are harness switches', switches(p.tree).length === 3)
|
|
1291
|
+
const modernRows = classed(p.tree, 'dbh-settings')[0]
|
|
1292
|
+
check('no legacy tick-box left in the settings rows', elements(modernRows, (el) => el.type === 'input' && String(el.props.className || '').includes('dbh-checkbox')).length === 0)
|
|
1293
|
+
clickClass(p.tree, 'dbh-linkbtn')
|
|
1294
|
+
p.tree = p.renderAgain()
|
|
1295
|
+
check('the same toggle collapses the rows again', count(p.tree, 'dbh-settings') === 0)
|
|
1296
|
+
p.dispose()
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
console.log('\n[28] modern phone browser: stacked layout, chips and settings still reachable')
|
|
1300
|
+
{
|
|
1301
|
+
viewport(390)
|
|
1302
|
+
const p = page(TILED, {
|
|
1303
|
+
slot: 'main',
|
|
1304
|
+
snapshot: Object.assign({}, MODERN_DEFAULTS),
|
|
1305
|
+
props: { useWorkspaces: wsSnap('D:/work/app') },
|
|
1306
|
+
})
|
|
1307
|
+
const html = toHtml(p.tree)
|
|
1308
|
+
check('mobile surface class applied', classed(p.tree, 'dbh-mobile').length === 1, html.slice(0, 200))
|
|
1309
|
+
check('phone layout is not the wide page', classed(p.tree, 'dbh-wide').length === 0)
|
|
1310
|
+
check('phone shows the title + version', count(p.tree, 'dbh-titlebar') === 1 && html.includes('v5.2.0-dev.1'))
|
|
1311
|
+
check('phone keeps the icon-only row actions', classed(p.tree, 'dbh-iconbtn').length === 8)
|
|
1312
|
+
check('read-only chips remain available on the phone', classed(p.tree, 'dbh-rochip').length === 3)
|
|
1313
|
+
if (count(p.tree, 'dbh-settings') === 0) {
|
|
1314
|
+
clickClass(p.tree, 'dbh-linkbtn')
|
|
1315
|
+
p.tree = p.renderAgain()
|
|
1316
|
+
}
|
|
1317
|
+
check('phone can open the settings rows', count(p.tree, 'dbh-srow') === 4)
|
|
1318
|
+
viewport(1440)
|
|
1319
|
+
p.dispose()
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
console.log('\n[29] dsh version gate: modern host fields on a 0.1.x runtime render the OLD page')
|
|
1323
|
+
{
|
|
1324
|
+
viewport(1440)
|
|
1325
|
+
const p = page(TILED, {
|
|
1326
|
+
slot: 'main',
|
|
1327
|
+
snapshot: Object.assign({}, MODERN_DEFAULTS, { dshModern: false }),
|
|
1328
|
+
props: { useWorkspaces: wsSnap('D:/work/app') },
|
|
1329
|
+
})
|
|
1330
|
+
const html = toHtml(p.tree)
|
|
1331
|
+
check('no modern class on a legacy runtime', classed(p.tree, 'dbh-modern').length === 0)
|
|
1332
|
+
check('no title bar on a legacy runtime', count(p.tree, 'dbh-titlebar') === 0)
|
|
1333
|
+
check('no settings rows on a legacy runtime', count(p.tree, 'dbh-settings') === 0)
|
|
1334
|
+
check('no inline read-only chip on a legacy runtime', classed(p.tree, 'dbh-rochip').length === 0)
|
|
1335
|
+
check('no harness Switch on a legacy runtime', switches(p.tree).length === 0)
|
|
1336
|
+
check('no update check dispatched on a legacy runtime', p.opCalls.filter((o) => o.op === 'updateCheck').length === 0, JSON.stringify(p.opCalls))
|
|
1337
|
+
check('legacy header still carries the gear + enable controls', html.includes('⚙') && html.includes('禁用'))
|
|
1338
|
+
check('legacy status text is unchanged (tool count stays)', html.includes('4 个工具'))
|
|
1339
|
+
p.dispose()
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
console.log('\n[30] narrow desktop window: non-modal pages keep the horizontal settings bar')
|
|
1343
|
+
{
|
|
1344
|
+
// 710px viewport: not a phone (>= 700) and not "wide" (< 720), which used to
|
|
1345
|
+
// fall back to four stacked rows — the desktop complaint. The bar must stay
|
|
1346
|
+
// horizontal, with its compact variant applied.
|
|
1347
|
+
viewport(710)
|
|
1348
|
+
const p = page(TILED, {
|
|
1349
|
+
slot: 'main',
|
|
1350
|
+
snapshot: Object.assign({}, MODERN_DEFAULTS),
|
|
1351
|
+
props: { useWorkspaces: wsSnap('D:/work/app') },
|
|
1352
|
+
})
|
|
1353
|
+
const html = toHtml(p.tree)
|
|
1354
|
+
check('narrow desktop still renders the settings bar', count(p.tree, 'dbh-settingsbar') === 1, html.slice(0, 200))
|
|
1355
|
+
check('narrow bar uses the compact variant', classed(p.tree, 'dbh-barnarrow').length === 1)
|
|
1356
|
+
check('no vertical settings rows on a narrow desktop', count(p.tree, 'dbh-srow') === 0)
|
|
1357
|
+
check('all four controls stay reachable', count(p.tree, 'dbh-bargroup') === 4 && switches(p.tree).length === 3)
|
|
1358
|
+
viewport(1440)
|
|
1359
|
+
p.dispose()
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1079
1362
|
if (liveMount) liveMount.dispose()
|
|
1080
1363
|
|
|
1081
1364
|
console.log(`\n${failures === 0 ? 'ALL CHECKS PASSED' : failures + ' CHECK(S) FAILED'}`)
|
|
1082
1365
|
process.exitCode = failures === 0 ? 0 : 1
|
|
1366
|
+
|
|
@@ -265,14 +265,16 @@ test('client bundle keeps the source handle copyable instead of echoing it inlin
|
|
|
265
265
|
assert.doesNotMatch(source, /w\.dsn/)
|
|
266
266
|
})
|
|
267
267
|
|
|
268
|
-
test('client bundle
|
|
268
|
+
test('client bundle draws clear row-action icons instead of text labels', () => {
|
|
269
269
|
assert.match(source, /function pickIcon\(name, glyph\)/)
|
|
270
|
-
|
|
270
|
+
// Test / edit / delete are explicit 16px SVG shapes (the bundled edit glyph is
|
|
271
|
+
// ambiguous at 1x); copy and close keep their product primitives.
|
|
272
|
+
assert.match(source, /var svgIcon = function \(children\)/)
|
|
273
|
+
assert.match(source, /var RunIcon = svgIcon\(/)
|
|
274
|
+
assert.match(source, /var EditIcon = svgIcon\(/)
|
|
275
|
+
assert.match(source, /var TrashIcon = svgIcon\(/)
|
|
271
276
|
assert.match(source, /pickIcon\("IconCopyOutline16"/)
|
|
272
|
-
assert.match(source, /pickIcon\("IconEditOutline16"/)
|
|
273
277
|
assert.match(source, /pickIcon\("IconCloseOutline16"/)
|
|
274
|
-
assert.match(source, /pickIcon\("IconTrashOutline16"/)
|
|
275
|
-
assert.match(source, /pickIcon\("IconPlayOutline16"/)
|
|
276
278
|
// icons carry their label in title/aria-label, never as row text
|
|
277
279
|
assert.match(source, /"aria-label": t\("btn\.test"\)/)
|
|
278
280
|
assert.match(source, /"aria-label": t\("btn\.delete"\)/)
|
|
@@ -429,3 +431,85 @@ test('client bundle disables what the installed dbhub cannot do', () => {
|
|
|
429
431
|
assert.match(source, /disabled: !sshSupported,/)
|
|
430
432
|
assert.match(source, /disabled: !roSupported,/)
|
|
431
433
|
})
|
|
434
|
+
|
|
435
|
+
test('client bundle gates every modern surface on the Host dsh version', () => {
|
|
436
|
+
// 5.2 surfaces (title/version header, update prompt, wide settings block,
|
|
437
|
+
// inline read-only toggle, switch-style controls) exist only on dsh >= 0.2.1.
|
|
438
|
+
// On 0.1.x the bundle must render the OLD DOM — the offscreen gate's legacy
|
|
439
|
+
// groups pin that, so the flag is read, never inferred from a slot existing.
|
|
440
|
+
assert.match(source, /var modern = value\.dshModern === true;/)
|
|
441
|
+
assert.match(source, /var pluginVersion = typeof value\.pluginVersion === "string"/)
|
|
442
|
+
assert.match(source, /var dshVersion = typeof value\.dshVersion === "string"/)
|
|
443
|
+
assert.match(source, /var runtime = value\.runtime === "desktop"/)
|
|
444
|
+
// The three mounts are named, not guessed: settings dialog vs full pages.
|
|
445
|
+
assert.match(source, /function SettingsSectionPanel\(props\)/)
|
|
446
|
+
assert.match(source, /function MainPanel\(props\)/)
|
|
447
|
+
assert.match(source, /surface: "modal"/)
|
|
448
|
+
assert.match(source, /surface: "main"/)
|
|
449
|
+
// Layout follows the measured column, with the phone check on the VIEWPORT
|
|
450
|
+
// (a narrow settings dialog on a desktop is not a phone).
|
|
451
|
+
assert.match(source, /function useMeasuredWidth\(ref\)/)
|
|
452
|
+
assert.match(source, /var isMobile = modern && \(viewportWidth > 0/)
|
|
453
|
+
assert.match(source, /var isWide = modern && !isMobile && surface !== "modal"/)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
test('client bundle shows the plugin version and the quiet update prompt', () => {
|
|
457
|
+
assert.match(source, /"ver\.link": "可升级到 \{v\}"/)
|
|
458
|
+
assert.match(source, /"ver\.link": "Upgrade to \{v\}"/)
|
|
459
|
+
assert.match(source, /function parseUpdate\(value\)/)
|
|
460
|
+
// A text link by default; the instructions live in a panel opened on demand.
|
|
461
|
+
assert.match(source, /className: "dbh-updatelink"/)
|
|
462
|
+
assert.match(source, /className: "dbh-updatepanel"/)
|
|
463
|
+
assert.match(source, /t\("ver\.copySpec", \{ v: update\.latest \}\)/)
|
|
464
|
+
// the copy control copies the exact install spec; the web command is copiable too
|
|
465
|
+
assert.match(source, /dsh-dbhub-live@" \+ update\.latest/)
|
|
466
|
+
assert.match(source, /dsh plugin --profile web up dsh-dbhub-live@/)
|
|
467
|
+
// opening the page asks the Host for a throttled check; the button forces one
|
|
468
|
+
assert.match(source, /op: "updateCheck", force: false/)
|
|
469
|
+
assert.match(source, /op: "updateCheck", force: force === true/)
|
|
470
|
+
// Updating is neutral news, not an error: no red card/pill classes remain.
|
|
471
|
+
assert.doesNotMatch(source, /dbh-latest/)
|
|
472
|
+
assert.doesNotMatch(source, /dbh-updatecard/)
|
|
473
|
+
// desktop and web update routes differ
|
|
474
|
+
assert.match(source, /t\("ver\.desktopHint"/)
|
|
475
|
+
assert.match(source, /t\("ver\.webHint"/)
|
|
476
|
+
assert.match(source, /releases\/tag\/v/)
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
test('client bundle renders DSH-style settings rows, the harness Switch and row read-only chips', () => {
|
|
480
|
+
assert.match(source, /function switchControl\(opts\)/)
|
|
481
|
+
// The real toggle comes from the primitives module; the fallback is local.
|
|
482
|
+
assert.match(source, /primitives\.Switch/)
|
|
483
|
+
assert.match(source, /className: "dbh-fbswitch"/)
|
|
484
|
+
assert.match(source, /className: "dbh-settings"/)
|
|
485
|
+
assert.match(source, /className: "dbh-srow"/)
|
|
486
|
+
// Non-modal pages always use the horizontal bar (a narrow desktop column gets
|
|
487
|
+
// the compact variant instead of four stacked rows).
|
|
488
|
+
assert.match(source, /var useSettingsBar = modern && !isMobile && surface !== "modal"/)
|
|
489
|
+
assert.match(source, /className: "dbh-settings dbh-settingsbar"/)
|
|
490
|
+
// State words are two-character, current-state labels — never "已开启".
|
|
491
|
+
assert.match(source, /"row\.enable": "启用插件"/)
|
|
492
|
+
assert.match(source, /"state\.show": "显示"/)
|
|
493
|
+
assert.match(source, /"state\.hide": "隐藏"/)
|
|
494
|
+
assert.match(source, /"state\.on": "开启"/)
|
|
495
|
+
assert.match(source, /"state\.off": "关闭"/)
|
|
496
|
+
// Wide page owns the settings: enable / sidebar / policy switches + interval.
|
|
497
|
+
assert.match(source, /props\.setEnabled\(next\)/)
|
|
498
|
+
assert.match(source, /onChange: toggleSidebar/)
|
|
499
|
+
assert.match(source, /onChange: togglePolicy/)
|
|
500
|
+
// Read-only is a row chip in every layout, written as an options-only op.
|
|
501
|
+
assert.match(source, /className: "dbh-rochip"/)
|
|
502
|
+
assert.match(source, /op: "options", workspace: w\.path, env: w\.env, readOnly: w\.ro !== true/)
|
|
503
|
+
// The chip shows an OPEN lock + 可写 when writable and a CLOSED lock + 只读
|
|
504
|
+
// when locked, so a first-time user cannot mistake two writable rows for RO.
|
|
505
|
+
assert.match(source, /"dbh-roemoji"/)
|
|
506
|
+
assert.match(source, /on \? "🔒" : "🔓"/)
|
|
507
|
+
assert.match(source, /t\(on \? "ro\.chip" : "ro\.writable"\)/)
|
|
508
|
+
assert.match(source, /"ro\.writable": "可写"/)
|
|
509
|
+
assert.match(source, /"ro\.writable": "Writable"/)
|
|
510
|
+
// Long explanations are collapsed into the notes block.
|
|
511
|
+
assert.match(source, /className: "dbh-notes"/)
|
|
512
|
+
assert.match(source, /"note\.tools":/)
|
|
513
|
+
assert.match(source, /className: "dbh-titlebar"/)
|
|
514
|
+
assert.match(source, /className: "dbh-title"/)
|
|
515
|
+
})
|