feat(web): add versioned first-run welcome

This commit is contained in:
NI0317
2026-07-30 22:17:56 +08:00
parent b6130b178d
commit bef8db3add
49 changed files with 1096 additions and 115 deletions

View File

@@ -1,12 +1,15 @@
/** Ownerless-copy registrations: the four seats, the dictionaries, locale refresh, and HMR recovery. */
import { Context } from 'cordis'
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings-general/client'
import type { GeneralSectionInjected } 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'
import { WelcomeNotice } from '../src/client/WelcomeNotice.tsx'
import type { WelcomeNoticeInjected } from '../src/client/WelcomeNotice.tsx'
import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../src/onboarding-copy.ts'
/** The four seats this plugin fills (slot name → expected component). */
const SEATS = [
@@ -14,6 +17,7 @@ const SEATS = [
['settings.header', HeaderContent],
['settings.close', CloseLabel],
['settings.section', GeneralSection],
['settings.onboarding', WelcomeNotice],
] as const
async function bench() {
@@ -21,7 +25,25 @@ async function bench() {
await ctx.plugin(SlotsService).await()
const locale = new LocaleService(ctx)
ctx.provide('locale', locale)
return { ctx, slots: ctx.get('slots') as SlotsService, locale }
const settingsDescribe = vi.fn(() => Promise.resolve({
rpcId: 'settings-general' as never,
result: {
ok: true as const,
value: {
writable: true,
namespaces: [{
ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
schema: {},
value: {},
applies: 'live' as const,
secrets: [],
revision: 0,
}],
},
},
}))
ctx.provide('connection', { api: { settings: { describe: settingsDescribe } } } as never)
return { ctx, slots: ctx.get('slots') as SlotsService, locale, settingsDescribe }
}
/** Declare the shell's four child slots the way ui-settings' entry does. */
@@ -34,6 +56,7 @@ function declare(slots: SlotsService): () => void {
'settings.header': { kind: 'single', scope: 'root' },
'settings.close': { kind: 'single', scope: 'root' },
'settings.section': { kind: 'list', scope: 'root' },
'settings.onboarding': { kind: 'list', scope: 'root' },
},
} as never,
() => null,
@@ -46,7 +69,7 @@ function generalEntry(slots: SlotsService) {
describe('ui-settings-general apply', () => {
it('declares the services it uses', () => {
expect(inject).toEqual(['slots', 'locale'])
expect(inject).toEqual(['slots', 'locale', 'connection'])
})
it('fills all four seats for declarations before or after apply', async () => {
@@ -61,6 +84,8 @@ describe('ui-settings-general apply', () => {
expect(before.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
const injected = (entry.inject as unknown as () => GeneralSectionInjected)()
expect(injected.t('permission.title')).toBe('权限')
const welcome = before.slots.entries('settings.onboarding')[0]!
expect(welcome.options).toEqual({ id: 'welcome-notice', order: -100 })
// The chrome seats share one inject face: the settings-ns translate.
const chrome = (before.slots.entries('settings.trigger')[0]!.inject as unknown as () => GeneralSectionInjected)()
expect(chrome.t('trigger')).toBe('设置')
@@ -116,6 +141,22 @@ describe('ui-settings-general apply', () => {
b.locale.setLocale('zh')
})
it('refreshes loaded welcome state only for its settings namespace or a reconnect', async () => {
const b = await bench()
declare(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
const entry = b.slots.entries('settings.onboarding')[0]!
const { controller } = (entry.inject as unknown as () => WelcomeNoticeInjected)()
await controller.load()
expect(b.settingsDescribe).toHaveBeenCalledOnce()
b.ctx.emit('settings/changed', 'unrelated')
expect(b.settingsDescribe).toHaveBeenCalledOnce()
b.ctx.emit('settings/changed', WELCOME_NOTICE_SETTINGS_NAMESPACE)
await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(2) })
b.ctx.emit('connection/reset')
await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(3) })
})
it('re-registers after an HMR collapse of the declaring chain (stale disposers must not block)', async () => {
const b = await bench()
const redeclare = declare(b.slots)