Merge origin/master into worktree/web-plugin-config, adapting to ctx.remote.$on
#2201 landed three moves this branch sits on top of. `bindSettingsScope` became `ctx.settingsScope.bind` on the Settings surface, so the section injects that service and the Remote face instead of importing a free function; the `SettingsScope` types moved into the runtime contract module, which is where this branch's `base`, `user`, and `unset` now live; and the client `credentials/changed` event became the forwarded owner event `credentials/updated`, which the web-search card subscribes to through `ctx.remote.$on`. The generated module graph crossed mermaid's 1000-edge render guard with this branch's 14 new edges. The guard doubles again, exactly as it did at 500 — the graph counts real package edges, and trimming them to fit a renderer default would make the document lie about the dependency set.
This commit is contained in:
@@ -4,7 +4,7 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { TestRemote, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings-general/client'
|
||||
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
|
||||
import { GeneralSection } from '../src/client/GeneralSection.tsx'
|
||||
@@ -33,6 +33,9 @@ async function bench(isLoopback = true) {
|
||||
await ctx.plugin(SlotsService).await()
|
||||
const locale = new LocaleService(ctx)
|
||||
ctx.provide('locale', locale)
|
||||
// The plugins inject `remote`; forwarded events reach them through the
|
||||
// same `$dispatch` handoff the connection sink makes.
|
||||
new TestRemote(ctx)
|
||||
const settingsDescribe = vi.fn(() => Promise.resolve({
|
||||
rpcId: 'settings-general' as never,
|
||||
result: {
|
||||
@@ -86,7 +89,7 @@ function generalEntry(slots: SlotsService) {
|
||||
|
||||
describe('ui-settings-general apply', () => {
|
||||
it('declares the services it uses', () => {
|
||||
expect(inject).toEqual(['slots', 'locale', 'connection'])
|
||||
expect(inject).toEqual(['slots', 'locale', 'connection', 'remote'])
|
||||
})
|
||||
|
||||
it('fills all six seats for declarations before or after apply', async () => {
|
||||
@@ -167,9 +170,9 @@ describe('ui-settings-general apply', () => {
|
||||
const { controller } = (entry.inject as unknown as () => WelcomeNoticeInjected)()
|
||||
await controller.load()
|
||||
expect(b.settingsDescribe).toHaveBeenCalledOnce()
|
||||
b.ctx.emit('settings/changed', 'unrelated')
|
||||
b.ctx.remote.$dispatch('settings/document-updated', ['unrelated', 1])
|
||||
expect(b.settingsDescribe).toHaveBeenCalledOnce()
|
||||
b.ctx.emit('settings/changed', WELCOME_NOTICE_SETTINGS_NAMESPACE)
|
||||
b.ctx.remote.$dispatch('settings/document-updated', [WELCOME_NOTICE_SETTINGS_NAMESPACE, 1])
|
||||
await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(2) })
|
||||
b.ctx.emit('connection/reset')
|
||||
await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(3) })
|
||||
|
||||
269
packages/client/ui-settings-general/tests/settings-root.spec.tsx
Normal file
269
packages/client/ui-settings-general/tests/settings-root.spec.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import type { SettingsRootComponentProps } from '../src/client/shell-contract.ts'
|
||||
import { SettingsRoot } from '../src/client/SettingsRoot.tsx'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
type Row = { id: string; order: number; label: string }
|
||||
type Step = { id: string; order: number }
|
||||
|
||||
/** Slot-content stand-ins: the shell renders whatever the seats contribute. */
|
||||
const SEAT_CONTENT: Record<string, string> = {
|
||||
'settings.trigger': 'Settings',
|
||||
'settings.header': 'Settings Title',
|
||||
'settings.action': 'Open configuration file',
|
||||
'settings.close': 'Close',
|
||||
}
|
||||
|
||||
function mount({
|
||||
wide = true,
|
||||
onboardingActive = true,
|
||||
rows = [
|
||||
{ id: 'general', order: 0, label: 'General' },
|
||||
{ id: 'models', order: 10, label: 'Models' },
|
||||
{ id: 'agent-presets', order: 20, label: 'Agent presets' },
|
||||
],
|
||||
steps = [
|
||||
{ id: 'welcome', order: -100 },
|
||||
{ id: 'credential', order: 0 },
|
||||
],
|
||||
}: { wide?: boolean; onboardingActive?: boolean; rows?: Row[]; steps?: Step[] } = {}) {
|
||||
// Mutable row source standing in for the bound useSections hook; bump()
|
||||
// plays a ledger change through the same observable contract.
|
||||
let current = rows
|
||||
const listeners = new Set<() => void>()
|
||||
const renderSlot = vi.fn(
|
||||
((key: string, _owner: unknown, opts?: { only?: string }) => {
|
||||
if (key === 'settings.section') return <div data-testid={`section-${opts?.only ?? 'all'}`} />
|
||||
return SEAT_CONTENT[key]
|
||||
}) as SettingsRootComponentProps['renderSlot'],
|
||||
)
|
||||
const useSessions = ((select: (state: unknown) => unknown) => select(onboardingActive
|
||||
? { phase: 'ready', current: undefined, byId: {} }
|
||||
: {
|
||||
phase: 'ready',
|
||||
current: 'active-session',
|
||||
byId: { 'active-session': { blank: false } },
|
||||
})) as never
|
||||
const unusedHook = (() => { throw new Error('unused by SettingsRoot') }) as never
|
||||
const props: SettingsRootComponentProps = {
|
||||
useSessions,
|
||||
useWorkspaces: unusedHook,
|
||||
wide,
|
||||
useOnboardingSteps: select => select(steps),
|
||||
useSections: (select) => {
|
||||
const [, force] = useState(0)
|
||||
useEffect(() => {
|
||||
const listener = () => { force(n => n + 1) }
|
||||
listeners.add(listener)
|
||||
return () => { listeners.delete(listener) }
|
||||
}, [])
|
||||
return select(current)
|
||||
},
|
||||
renderSlot,
|
||||
}
|
||||
const view = render(<SettingsRoot {...props} />)
|
||||
const bump = (next: Row[]) => {
|
||||
act(() => {
|
||||
current = next
|
||||
for (const fn of [...listeners]) fn()
|
||||
})
|
||||
}
|
||||
return { view, renderSlot, bump, listeners }
|
||||
}
|
||||
|
||||
function openPanel() {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Settings' }))
|
||||
}
|
||||
|
||||
describe('SettingsRoot trigger', () => {
|
||||
it('renders the trigger seat content as the accessible name (no aria-label of its own)', () => {
|
||||
const { renderSlot } = mount()
|
||||
const trigger = screen.getByRole('button', { name: 'Settings' })
|
||||
expect(trigger.hasAttribute('aria-label')).toBe(false)
|
||||
expect(renderSlot).toHaveBeenCalledWith('settings.trigger', { wide: true })
|
||||
expect(trigger.getAttribute('aria-expanded')).toBe('false')
|
||||
fireEvent.click(trigger)
|
||||
expect(screen.getByRole('dialog')).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'Settings', expanded: true })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('hands the rail state to the trigger seat', () => {
|
||||
const { renderSlot } = mount({ wide: false })
|
||||
expect(renderSlot).toHaveBeenCalledWith('settings.trigger', { wide: false })
|
||||
})
|
||||
})
|
||||
|
||||
describe('SettingsPanel chrome seats', () => {
|
||||
it('names the dialog via aria-labelledby pointing at the header seat node', () => {
|
||||
mount()
|
||||
openPanel()
|
||||
const dialog = screen.getByRole('dialog')
|
||||
const titleId = dialog.getAttribute('aria-labelledby')!
|
||||
expect(titleId).toBeTruthy()
|
||||
const title = document.getElementById(titleId)!
|
||||
expect(title.textContent).toBe('Settings Title')
|
||||
expect(screen.getByRole('dialog', { name: 'Settings Title' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('names the close button through the visually-hidden close seat text', () => {
|
||||
mount()
|
||||
openPanel()
|
||||
const close = screen.getByRole('button', { name: 'Close' })
|
||||
expect(close.hasAttribute('aria-label')).toBe(false)
|
||||
expect(close.textContent).toContain('Close')
|
||||
})
|
||||
|
||||
it('renders header actions before the shell-owned close control', () => {
|
||||
const { renderSlot } = mount()
|
||||
openPanel()
|
||||
expect(screen.getByText('Open configuration file')).toBeTruthy()
|
||||
expect(renderSlot).toHaveBeenCalledWith('settings.action', {})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SettingsPanel close paths', () => {
|
||||
it('closes via the header button', () => {
|
||||
mount()
|
||||
openPanel()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
})
|
||||
|
||||
it('closes via a mask click', () => {
|
||||
mount()
|
||||
openPanel()
|
||||
const dialog = screen.getByRole('dialog')
|
||||
fireEvent.click(dialog.parentElement!.firstElementChild!)
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
})
|
||||
|
||||
it('closes via document-level Escape and unhooks the listener with the panel', () => {
|
||||
mount()
|
||||
openPanel()
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
// Ignored while closed (listener removed with the panel) and non-Escape
|
||||
// keys are ignored while open.
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
openPanel()
|
||||
fireEvent.keyDown(document, { key: 'Enter' })
|
||||
expect(screen.getByRole('dialog')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('lands focus on the close button when the dialog opens', () => {
|
||||
mount()
|
||||
openPanel()
|
||||
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Close' }))
|
||||
})
|
||||
})
|
||||
|
||||
describe('SettingsPanel navigation', () => {
|
||||
it('projects rows, marks the first active, and renders only that section', () => {
|
||||
mount()
|
||||
openPanel()
|
||||
expect(screen.getByRole('button', { name: 'General' }).getAttribute('aria-current')).toBe('true')
|
||||
expect(screen.getByRole('button', { name: 'Models' }).getAttribute('aria-current')).toBeNull()
|
||||
expect(screen.getByTestId('section-general')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('gives every section a nav glyph, distinct for the ids the shell knows', () => {
|
||||
mount({
|
||||
rows: [
|
||||
{ id: 'general', order: 0, label: 'General' },
|
||||
{ id: 'models', order: 10, label: 'Models' },
|
||||
{ id: 'agent-presets', order: 20, label: 'Agent presets' },
|
||||
{ id: 'plugins', order: 30, label: 'Plugins' },
|
||||
{ id: 'contributed', order: 40, label: 'Contributed' },
|
||||
],
|
||||
})
|
||||
openPanel()
|
||||
// Glyphs carry no id of their own, so the drawn paths are what tells them apart.
|
||||
const glyphs = ['General', 'Models', 'Agent presets', 'Plugins', 'Contributed']
|
||||
.map(name => screen.getByRole('button', { name }).querySelector('svg')?.innerHTML)
|
||||
|
||||
expect(glyphs.every(glyph => glyph !== undefined && glyph !== '')).toBe(true)
|
||||
// The three ids the shell names get their own glyph; every other section —
|
||||
// including one this package never heard of — shares the gear.
|
||||
expect(new Set(glyphs.slice(0, 4)).size).toBe(4)
|
||||
expect(glyphs[4]).toBe(glyphs[0])
|
||||
})
|
||||
|
||||
it('switches the rendered section on nav click', () => {
|
||||
mount()
|
||||
openPanel()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Models' }))
|
||||
expect(screen.getByRole('button', { name: 'Models' }).getAttribute('aria-current')).toBe('true')
|
||||
expect(screen.getByTestId('section-models')).toBeTruthy()
|
||||
expect(screen.queryByTestId('section-general')).toBeNull()
|
||||
})
|
||||
|
||||
it('mounts onboarding steps in order and transfers ownership only on completion', () => {
|
||||
const { renderSlot } = mount()
|
||||
const first = renderSlot.mock.calls.find(call => call[0] === 'settings.onboarding')
|
||||
expect(first?.[1]).toMatchObject({ stepId: 'welcome' })
|
||||
expect(first?.[2]).toEqual({ only: 'welcome' })
|
||||
act(() => {
|
||||
(first?.[1] as { complete: () => void }).complete()
|
||||
;(first?.[1] as { complete: () => void }).complete()
|
||||
})
|
||||
const onboardingCalls = renderSlot.mock.calls.filter(call => call[0] === 'settings.onboarding')
|
||||
const second = onboardingCalls.at(-1)
|
||||
expect(second?.[1]).toMatchObject({ stepId: 'credential' })
|
||||
expect(second?.[2]).toEqual({ only: 'credential' })
|
||||
|
||||
act(() => {
|
||||
(second?.[1] as { openSection: (id: string) => void }).openSection('models')
|
||||
})
|
||||
expect(screen.getByRole('dialog')).toBeTruthy()
|
||||
expect(screen.getByTestId('section-models')).toBeTruthy()
|
||||
|
||||
cleanup()
|
||||
const inactive = mount({ onboardingActive: false }).renderSlot.mock.calls
|
||||
.filter(call => call[0] === 'settings.onboarding')
|
||||
expect(inactive).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('paints no takeover chrome of its own around the mounted step', () => {
|
||||
// The chrome (mask, opaque stage, #root inert) belongs to the step via
|
||||
// the OnboardingSurface primitive — a mounted-but-deciding step that
|
||||
// renders null must show and block nothing (the reload white-flash fix;
|
||||
// onboarding-surface.spec.tsx pins the primitive's half).
|
||||
const appRoot = document.createElement('div')
|
||||
appRoot.id = 'root'
|
||||
document.body.append(appRoot)
|
||||
const { view } = mount()
|
||||
expect(view.container.querySelector('[class*="onboarding"]')).toBeNull()
|
||||
expect(document.body.querySelector('[class*="onboarding"]')).toBeNull()
|
||||
expect(appRoot.inert).not.toBe(true)
|
||||
view.unmount()
|
||||
appRoot.remove()
|
||||
})
|
||||
|
||||
it('falls back to the first row when the active entry unregisters', () => {
|
||||
const { bump } = mount()
|
||||
openPanel()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Models' }))
|
||||
bump([{ id: 'general', order: 0, label: 'General' }])
|
||||
expect(screen.queryByRole('button', { name: 'Models' })).toBeNull()
|
||||
expect(screen.getByTestId('section-general')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders an empty content column when the ledger is empty', () => {
|
||||
const { renderSlot } = mount({ rows: [] })
|
||||
openPanel()
|
||||
expect(screen.getByRole('dialog')).toBeTruthy()
|
||||
const sectionCalls = renderSlot.mock.calls.filter(c => c[0] === 'settings.section')
|
||||
expect(sectionCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('drops the ledger subscription on unmount', () => {
|
||||
const { view, listeners } = mount()
|
||||
expect(listeners.size).toBe(1)
|
||||
view.unmount()
|
||||
expect(listeners.size).toBe(0)
|
||||
})
|
||||
})
|
||||
157
packages/client/ui-settings-general/tests/shell.spec.ts
Normal file
157
packages/client/ui-settings-general/tests/shell.spec.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/** Settings shell registration: slot declaration injection, the ledger projections, and HMR recovery. */
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { apply, inject } from '../src/client/index.ts'
|
||||
import type { SettingsRootInjected } from '../src/client/shell-contract.ts'
|
||||
import { SettingsRoot } from '../src/client/SettingsRoot.tsx'
|
||||
|
||||
async function bench() {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SlotsService).await()
|
||||
// Copy machinery the shell only reads a revision from; the real locale
|
||||
// plugin would drag its own settings-row dependencies into this bench.
|
||||
ctx.provide('locale', {
|
||||
register: () => () => {},
|
||||
bind: () => (key: string) => key,
|
||||
getSnapshot: () => ({ active: 'zh', locales: [], revision: 0 }),
|
||||
subscribe: () => () => {},
|
||||
} as never)
|
||||
ctx.provide('connection', {
|
||||
api: { settings: { describe: async () => ({ result: { ok: false } }) } },
|
||||
isLoopback: false,
|
||||
} as never)
|
||||
ctx.provide('remote', { $on: () => () => {} } as never)
|
||||
return { ctx, slots: ctx.get('slots') as SlotsService }
|
||||
}
|
||||
|
||||
function declare(slots: SlotsService): () => void {
|
||||
return slots.register(
|
||||
{ name: 'root', children: { 'sidebar.settings': { kind: 'single', scope: 'root' } } } as never,
|
||||
() => null,
|
||||
)
|
||||
}
|
||||
|
||||
function injectedOf(slots: SlotsService): SettingsRootInjected {
|
||||
const entry = slots.entries('sidebar.settings')[0]!
|
||||
return (entry.inject as () => SettingsRootInjected)()
|
||||
}
|
||||
|
||||
/** The shell's child declarations (chrome, actions, sections, and onboarding overlays). */
|
||||
const CHILD_SPECS = {
|
||||
'settings.trigger': { kind: 'single', scope: 'root' },
|
||||
'settings.header': { kind: 'single', scope: 'root' },
|
||||
'settings.action': { kind: 'list', scope: 'root' },
|
||||
'settings.close': { kind: 'single', scope: 'root' },
|
||||
'settings.section': { kind: 'list', scope: 'root' },
|
||||
'settings.onboarding': { kind: 'list', scope: 'root' },
|
||||
} as const
|
||||
|
||||
describe('ui-settings apply', () => {
|
||||
it('declares only the slot registry (a pure composition face, no locale)', () => {
|
||||
expect(inject).toEqual(['slots', 'locale', 'connection', 'remote'])
|
||||
})
|
||||
|
||||
it('registers the shell and declares every child slot, before or after the declaration', async () => {
|
||||
const before = await bench()
|
||||
declare(before.slots)
|
||||
await before.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
expect(before.slots.entries('sidebar.settings')[0]!.component).toBe(SettingsRoot)
|
||||
for (const name of Object.keys(CHILD_SPECS) as Array<keyof typeof CHILD_SPECS>) {
|
||||
expect(before.slots.spec(name)).toEqual(CHILD_SPECS[name])
|
||||
}
|
||||
|
||||
const after = await bench()
|
||||
await after.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
expect(after.slots.entries('sidebar.settings')).toHaveLength(0)
|
||||
declare(after.slots)
|
||||
await Promise.resolve()
|
||||
expect(after.slots.entries('sidebar.settings')[0]!.component).toBe(SettingsRoot)
|
||||
// The self-inflicted ledger notifications hit the duplicate guard.
|
||||
expect(after.slots.entries('sidebar.settings')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('projects the section ledger into ordered nav rows with option defaults', async () => {
|
||||
const b = await bench()
|
||||
declare(b.slots)
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
const { sections } = injectedOf(b.slots).hooks
|
||||
// This package registers the General section itself; every other section
|
||||
// arrives from a feature registrant.
|
||||
const GENERAL = { id: 'general', order: 0, label: 'general.nav' }
|
||||
expect(sections.getSnapshot()).toEqual([GENERAL])
|
||||
b.slots.register({ name: 'settings.section', id: 'z', order: 20, label: 'Z' } as never, () => null)
|
||||
// No order and no label: both projection defaults apply.
|
||||
b.slots.register({ name: 'settings.section', id: 'a' } as never, () => null)
|
||||
const rows = sections.getSnapshot()
|
||||
expect(rows).toEqual([
|
||||
GENERAL,
|
||||
{ id: 'a', order: 0, label: '' },
|
||||
{ id: 'z', order: 20, label: 'Z' },
|
||||
])
|
||||
// Snapshot identity is stable until the ledger moves (uSES contract).
|
||||
expect(sections.getSnapshot()).toBe(rows)
|
||||
const listener = vi.fn()
|
||||
const off = sections.subscribe(listener)
|
||||
b.slots.register({ name: 'settings.section', id: 'b', order: 1, label: 'B' } as never, () => null)
|
||||
await Promise.resolve()
|
||||
expect(listener).toHaveBeenCalled()
|
||||
expect(sections.getSnapshot()).not.toBe(rows)
|
||||
off()
|
||||
})
|
||||
|
||||
it('projects onboarding entries into stable coordinator order', async () => {
|
||||
const b = await bench()
|
||||
declare(b.slots)
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
const { onboardingSteps } = injectedOf(b.slots).hooks
|
||||
b.slots.register({ name: 'settings.onboarding', id: 'credential', order: 0 } as never, () => null)
|
||||
b.slots.register({ name: 'settings.onboarding', id: 'welcome', order: -100 } as never, () => null)
|
||||
b.slots.register({ name: 'settings.onboarding', id: 'default-order' } as never, () => null)
|
||||
const steps = onboardingSteps.getSnapshot()
|
||||
expect(steps).toEqual([
|
||||
// This package's own onboarding page, registered by the same apply.
|
||||
{ id: 'welcome-notice', order: -100 },
|
||||
{ id: 'welcome', order: -100 },
|
||||
{ id: 'credential', order: 0 },
|
||||
{ id: 'default-order', order: 0 },
|
||||
])
|
||||
expect(onboardingSteps.getSnapshot()).toBe(steps)
|
||||
const listener = vi.fn()
|
||||
const off = onboardingSteps.subscribe(listener)
|
||||
b.slots.register({ name: 'settings.onboarding', id: 'later', order: 10 } as never, () => null)
|
||||
await Promise.resolve()
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
off()
|
||||
})
|
||||
|
||||
it('re-registers after an HMR collapse re-declares the slot (stale disposer must not block)', async () => {
|
||||
const b = await bench()
|
||||
const redeclare = declare(b.slots)
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
expect(b.slots.entries('sidebar.settings')).toHaveLength(1)
|
||||
// Declarer unload: the cascade removes our entry and every child
|
||||
// declaration while our local disposer variable goes stale.
|
||||
redeclare()
|
||||
expect(b.slots.entries('sidebar.settings')).toHaveLength(0)
|
||||
expect(b.slots.spec('settings.trigger')).toBeUndefined()
|
||||
declare(b.slots)
|
||||
await Promise.resolve()
|
||||
expect(b.slots.entries('sidebar.settings')[0]!.component).toBe(SettingsRoot)
|
||||
for (const name of Object.keys(CHILD_SPECS) as Array<keyof typeof CHILD_SPECS>) {
|
||||
expect(b.slots.spec(name)).toEqual(CHILD_SPECS[name])
|
||||
}
|
||||
})
|
||||
|
||||
it('unregisters the shell and collapses every child slot on teardown', async () => {
|
||||
const b = await bench()
|
||||
declare(b.slots)
|
||||
const fiber = b.ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
await fiber.dispose()
|
||||
expect(b.slots.entries('sidebar.settings')).toHaveLength(0)
|
||||
for (const name of Object.keys(CHILD_SPECS) as Array<keyof typeof CHILD_SPECS>) {
|
||||
expect(b.slots.spec(name)).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user