Files
dsh-context/tests/client/settingsJump.spec.ts
T
bowenliang123 59cd1be501 feat(client): expand the plugin settings card on the Open-in-Settings jump
The settings shell keeps its open/section state in private component
state and offers no plugin-facing open face, but the card registered on
settings.plugin.item is ours. After the jump's settings-trigger click
survives, it raises a short-lived in-bundle expand request; the card
consumes it on mount and lands open, scrolled into view. Degraded jumps
raise no request, and the request expires in 5s, so no stale signal
surfaces on a later manual visit.
2026-08-29 13:32:09 +08:00

175 lines
6.5 KiB
TypeScript

// openPluginSettings (src/client/settingsJump.ts): the guarded DOM jump to
// the plugin's settings page — settings trigger first, Plugins section nav
// row second — plus the hostile degradations (missing chrome, throwing
// elements, already-open panel) and the expand request the surviving jump
// leaves for the plugin's settings card. jsdom supplies the real document.
import assert from 'node:assert/strict'
import { afterEach, describe, test } from 'vitest'
import { consumeCardExpand, openPluginSettings, requestCardExpand } from '../../src/client/settingsJump'
/** Immediate scheduler: captures the deferred runs in issue order. */
function syncSchedule(): { runs: Array<() => void>; schedule: (run: () => void, ms: number) => void } {
const runs: Array<() => void> = []
return { runs, schedule: (run) => { runs.push(run) } }
}
/** A sidebar-style settings trigger (dialog semantics + expanded flag). */
function trigger(doc: Document, expanded: 'true' | 'false'): HTMLButtonElement {
const b = doc.createElement('button')
b.setAttribute('aria-haspopup', 'dialog')
b.setAttribute('aria-expanded', expanded)
doc.body.appendChild(b)
return b
}
/** Count native click dispatches on an element. */
function clicks(el: HTMLElement): { count: () => number } {
let n = 0
el.addEventListener('click', () => { n++ })
return { count: () => n }
}
describe('openPluginSettings', () => {
afterEach(() => {
document.body.textContent = ''
consumeCardExpand()
})
test('clicks the collapsed trigger, then the Plugins section row', () => {
const t = trigger(document, 'false')
const tClicks = clicks(t)
const section = document.createElement('button')
section.textContent = '插件'
document.body.appendChild(section)
const sClicks = clicks(section)
const { runs, schedule } = syncSchedule()
openPluginSettings(document, schedule)
assert.equal(tClicks.count(), 1, 'the collapsed trigger is clicked immediately')
assert.equal(sClicks.count(), 0, 'the section row waits for the panel to open')
assert.equal(runs.length, 1)
runs[0]()
assert.equal(sClicks.count(), 1, 'the Plugins nav row is clicked once the panel opens')
})
test('the en-labeled section row matches too', () => {
trigger(document, 'false')
const section = document.createElement('button')
section.textContent = 'Plugins'
document.body.appendChild(section)
const sClicks = clicks(section)
const { runs, schedule } = syncSchedule()
openPluginSettings(document, schedule)
runs[0]()
assert.equal(sClicks.count(), 1)
})
test('the default schedule defers through window.setTimeout', async () => {
trigger(document, 'false')
const section = document.createElement('button')
section.textContent = '插件'
document.body.appendChild(section)
const sClicks = clicks(section)
openPluginSettings(document)
await new Promise(resolve => { setTimeout(resolve, 120) })
assert.equal(sClicks.count(), 1)
})
test('an already-open panel skips the trigger but still selects the section', () => {
const t = trigger(document, 'true')
const tClicks = clicks(t)
const section = document.createElement('button')
section.textContent = '插件'
document.body.appendChild(section)
const sClicks = clicks(section)
const { runs, schedule } = syncSchedule()
openPluginSettings(document, schedule)
runs[0]()
assert.equal(tClicks.count(), 0, 'no re-click on an open panel')
assert.equal(sClicks.count(), 1)
})
test('a chrome with no settings trigger degrades to a silent no-op', () => {
const { runs, schedule } = syncSchedule()
assert.doesNotThrow(() => openPluginSettings(document, schedule))
assert.equal(runs.length, 0, 'no deferred section selection without a trigger')
})
test('elements that throw on click never surface the error', () => {
const t = trigger(document, 'false')
t.click = () => { throw new Error('detached') }
const { runs, schedule } = syncSchedule()
assert.doesNotThrow(() => openPluginSettings(document, schedule))
// The trigger's failure aborts this jump entirely — the guarded path.
assert.equal(runs.length, 0)
})
test('a throwing section row is contained by its own guard', () => {
trigger(document, 'false')
const section = document.createElement('button')
section.textContent = '插件'
section.click = () => { throw new Error('detached') }
document.body.appendChild(section)
const { runs, schedule } = syncSchedule()
assert.doesNotThrow(() => openPluginSettings(document, schedule))
assert.doesNotThrow(() => runs[0]())
})
test('lookalike chrome is ignored: a menu trigger is not the settings dialog', () => {
const menu = document.createElement('button')
menu.setAttribute('aria-haspopup', 'menu')
menu.setAttribute('aria-expanded', 'false')
document.body.appendChild(menu)
const menuClicks = clicks(menu)
const { runs, schedule } = syncSchedule()
openPluginSettings(document, schedule)
assert.equal(menuClicks.count(), 0, 'only dialog triggers are candidates')
assert.equal(runs.length, 0)
})
test('a surviving jump leaves a fresh expand request for the settings card', () => {
trigger(document, 'false')
const section = document.createElement('button')
section.textContent = '插件'
document.body.appendChild(section)
const { schedule } = syncSchedule()
openPluginSettings(document, schedule)
assert.equal(consumeCardExpand(), true, 'the card mounts expanded on this jump')
assert.equal(consumeCardExpand(), false, 'the request is consumed once')
})
test('degraded jumps leave no expand request behind', () => {
const { runs, schedule } = syncSchedule()
openPluginSettings(document, schedule)
assert.equal(consumeCardExpand(), false, 'no settings dialog in this chrome: no request')
const t = trigger(document, 'false')
t.click = () => { throw new Error('detached') }
assert.doesNotThrow(() => openPluginSettings(document, schedule))
assert.equal(consumeCardExpand(), false, 'a failed trigger click leaves no stale request')
assert.equal(runs.length, 0)
})
test('the expand request is freshness-windowed (5s) and single-shot', () => {
requestCardExpand(1000)
assert.equal(consumeCardExpand(1000 + 4999), true, 'fresh within the window')
assert.equal(consumeCardExpand(1000 + 4999), false, 'already consumed')
requestCardExpand(1000)
assert.equal(consumeCardExpand(1000 + 5000), false, 'stale past the window')
})
})