Files
dsh-context/tests/client/placement.spec.ts
T
bowenliang123 0f01900a91 fix(client): track a placement mount by its own state, not its disposer
watchPlacement inferred "this side is mounted" from the mount handle
being a function, so a face that seats the seat and hands back no disposer
(a stripped or foreign `slots`) read as never mounted — and every later
preference publish registered a second copy of the same seats. Track the
mounted state directly; the handle is unwound when there is one.
2026-10-12 08:39:50 +08:00

135 lines
5.2 KiB
TypeScript

// Placement gating (src/client/placement.ts): the mounts follow the `defaultPlacement` preference — both, the tab
// only, or the sidebar only — and a flip disposes exactly what the new value drops.
import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
import { createContextSettings } from '../../src/client/settings'
import { watchPlacement } from '../../src/client/placement'
/** A mount factory counting its mounts and the disposer calls it received. */
function mountSpy(): { factory: () => () => void; mounted: () => number; disposed: () => number } {
let mountedCount = 0
let disposedCount = 0
return {
factory: () => {
mountedCount += 1
return () => { disposedCount += 1 }
},
mounted: () => mountedCount,
disposed: () => disposedCount,
}
}
describe('watchPlacement', () => {
test("the default 'all' mounts both; a flip to one side disposes only the other", () => {
const settings = createContextSettings()
const tab = mountSpy()
const sidebar = mountSpy()
const dispose = watchPlacement(settings, { tab: tab.factory, sidebar: sidebar.factory })
assert.equal(tab.mounted(), 1)
assert.equal(sidebar.mounted(), 1)
settings.set('defaultPlacement', 'sidebar')
assert.equal(tab.disposed(), 1, 'the dropped tab registration was disposed')
assert.equal(tab.mounted(), 1, 'no remount churn')
assert.equal(sidebar.disposed(), 0, 'the sidebar stays mounted')
assert.equal(sidebar.mounted(), 1)
settings.set('defaultPlacement', 'tab')
assert.equal(sidebar.disposed(), 1)
assert.equal(sidebar.mounted(), 1)
assert.equal(tab.mounted(), 2, 'the tab comes back')
assert.equal(tab.disposed(), 1)
dispose()
})
test("'all' restores both; unrelated preference changes never remount", () => {
const settings = createContextSettings()
const tab = mountSpy()
const sidebar = mountSpy()
const dispose = watchPlacement(settings, { tab: tab.factory, sidebar: sidebar.factory })
settings.set('defaultGranularity', 'turn')
assert.equal(tab.mounted(), 1)
assert.equal(sidebar.mounted(), 1)
settings.set('defaultPlacement', 'tab')
settings.set('defaultPlacement', 'all')
assert.equal(tab.mounted(), 1, 'the kept side is never churned')
assert.equal(sidebar.mounted(), 2, 'the dropped side remounts once')
assert.equal(sidebar.disposed(), 1)
dispose()
})
test("a 'tab'-only or 'sidebar'-only start mounts just that side", () => {
const settings = createContextSettings()
settings.set('defaultPlacement', 'tab')
const tab = mountSpy()
const sidebar = mountSpy()
const dispose = watchPlacement(settings, { tab: tab.factory, sidebar: sidebar.factory })
assert.equal(tab.mounted(), 1)
assert.equal(sidebar.mounted(), 0)
dispose()
assert.equal(tab.disposed(), 1)
assert.equal(sidebar.disposed(), 0)
const again = createContextSettings()
again.set('defaultPlacement', 'sidebar')
const tab2 = mountSpy()
const sidebar2 = mountSpy()
const dispose2 = watchPlacement(again, { tab: tab2.factory, sidebar: sidebar2.factory })
assert.equal(tab2.mounted(), 0)
assert.equal(sidebar2.mounted(), 1)
dispose2()
})
test('the watcher disposer unwinds every mount and detaches the store', () => {
const settings = createContextSettings()
const tab = mountSpy()
const sidebar = mountSpy()
const dispose = watchPlacement(settings, { tab: tab.factory, sidebar: sidebar.factory })
dispose()
assert.equal(tab.disposed(), 1)
assert.equal(sidebar.disposed(), 1)
// The store subscription is gone: a later flip reaches no mount.
settings.set('defaultPlacement', 'sidebar')
assert.equal(tab.mounted(), 1)
assert.equal(sidebar.mounted(), 1)
// The disposer is safe to repeat.
assert.doesNotThrow(dispose)
assert.equal(tab.disposed(), 1, 'no double unwind')
})
test('a mount landing no disposer (a face returning nothing) is tolerated', () => {
const settings = createContextSettings()
const dispose = watchPlacement(settings, {
tab: () => undefined,
sidebar: () => undefined,
})
assert.doesNotThrow(() => { settings.set('defaultPlacement', 'tab') })
assert.doesNotThrow(() => { dispose() })
})
test('a disposerless mount is still counted as mounted — later publishes never register a second copy', () => {
const settings = createContextSettings()
let tabs = 0
let sidebars = 0
watchPlacement(settings, {
// A face that seats the seat but hands back no disposer (a stripped or foreign `slots`).
tab: () => { tabs += 1; return undefined },
sidebar: () => { sidebars += 1; return undefined },
})
assert.equal(tabs, 1)
assert.equal(sidebars, 1)
// Unrelated preference publishes leave the mounted sides alone.
settings.set('defaultGranularity', 'turn')
settings.set('defaultPlacement', 'tab')
assert.equal(tabs, 1, 'the tab is never mounted twice')
assert.equal(sidebars, 1, 'the sidebar unwound once and does not come back on its own')
// The dropped side can come back, and comes back exactly once.
settings.set('defaultPlacement', 'all')
assert.equal(tabs, 1)
assert.equal(sidebars, 2)
})
})