feat(web): add versioned first-run welcome
This commit is contained in:
@@ -83,6 +83,29 @@ describe('ui-settings apply', () => {
|
||||
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([
|
||||
{ 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)
|
||||
|
||||
@@ -8,6 +8,7 @@ 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> = {
|
||||
@@ -23,7 +24,11 @@ function mount({
|
||||
{ id: 'general', order: 0, label: 'General' },
|
||||
{ id: 'models', order: 10, label: 'Models' },
|
||||
],
|
||||
}: { wide?: boolean; onboardingActive?: boolean; rows?: Row[] } = {}) {
|
||||
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
|
||||
@@ -46,6 +51,7 @@ function mount({
|
||||
useSessions,
|
||||
useWorkspaces: unusedHook,
|
||||
wide,
|
||||
useOnboardingSteps: select => select(steps),
|
||||
useSections: (select) => {
|
||||
const [, force] = useState(0)
|
||||
useEffect(() => {
|
||||
@@ -164,20 +170,30 @@ describe('SettingsPanel navigation', () => {
|
||||
expect(screen.queryByTestId('section-general')).toBeNull()
|
||||
})
|
||||
|
||||
it('hands Hero readiness and a direct section opener to onboarding registrants', () => {
|
||||
it('mounts onboarding steps in order and transfers ownership only on completion', () => {
|
||||
const { renderSlot } = mount()
|
||||
const onboardingCall = renderSlot.mock.calls.find(call => call[0] === 'settings.onboarding')
|
||||
expect(onboardingCall?.[1]).toMatchObject({ active: true })
|
||||
const first = renderSlot.mock.calls.find(call => call[0] === 'settings.onboarding')
|
||||
expect(first?.[1]).toMatchObject({ stepId: 'welcome' })
|
||||
expect(first?.[2]).toEqual({ only: 'welcome' })
|
||||
act(() => {
|
||||
(onboardingCall?.[1] as { openSection: (id: string) => void }).openSection('models')
|
||||
(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 active = mount({ onboardingActive: false }).renderSlot.mock.calls
|
||||
.find(call => call[0] === 'settings.onboarding')
|
||||
expect(active?.[1]).toMatchObject({ active: false })
|
||||
const inactive = mount({ onboardingActive: false }).renderSlot.mock.calls
|
||||
.filter(call => call[0] === 'settings.onboarding')
|
||||
expect(inactive).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('falls back to the first row when the active entry unregisters', () => {
|
||||
|
||||
Reference in New Issue
Block a user