|
|
@@ -1,20 +1,30 @@
|
|
|
-/** Ownerless-copy registrations: the five seats, dictionaries, thunked labels, and HMR recovery. */
|
|
|
-import { Context } from '@deepseek-ai/cordis'
|
|
|
-import { describe, expect, it, vi } from 'vitest'
|
|
|
+// @vitest-environment jsdom
|
|
|
+/**
|
|
|
+ * Ownerless-copy registrations inside the assembled web client: the five
|
|
|
+ * seats, the `settings` dictionaries, the locale-following nav label, the
|
|
|
+ * loopback-only document action over the real settings mirror, and recovery
|
|
|
+ * across Loader rebuilds of the declaring chain.
|
|
|
+ */
|
|
|
+import { describe, expect, onTestFinished, vi } from 'vitest'
|
|
|
+import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
|
+import { ok, type RemoteMock } from '@deepseek-ai/dsh-remote-mock'
|
|
|
+import type { SettingsNamespaceView } from '@deepseek-ai/dsh-settings/types'
|
|
|
+import { createClientTest, type TestClient, webApp } from '@deepseek-ai/dsh-client-test-runtime/src/assembly/index.ts'
|
|
|
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
|
|
|
-import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
|
-import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
|
|
-import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
|
|
-import { apply as settingsApply, inject as settingsInject } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
|
-import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings-general/client'
|
|
|
+import { LOCALE_SETTINGS_NAMESPACE, LocaleSettingsSchema } from '@deepseek-ai/dsh-client-locale/src/locale-settings.ts'
|
|
|
+import { inject } from '../src/client/index.ts'
|
|
|
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
|
|
|
import { GeneralSection } from '../src/client/GeneralSection.tsx'
|
|
|
import { SettingsDocumentAction } from '../src/client/SettingsDocumentAction.tsx'
|
|
|
import type { SettingsDocumentActionInjected } from '../src/client/SettingsDocumentAction.tsx'
|
|
|
|
|
|
-// These specs assert the shipped Chinese copy. The lane has no jsdom `window`,
|
|
|
-// so browser-language detection never runs and a fresh LocaleRuntime opens on
|
|
|
-// FALLBACK_LOCALE (en); bench stages zh explicitly on the locale instead.
|
|
|
+const SELF = '@deepseek-ai/dsh-client-ui-settings-general'
|
|
|
+const SIDEBAR = '@deepseek-ai/dsh-client-ui-sidebar'
|
|
|
+const it = createClientTest({ roster: webApp })
|
|
|
+/** The whole roster's first boot pays the cold module transform of every plugin package. */
|
|
|
+const COLD_BOOT_TIMEOUT_MS = 60_000
|
|
|
+/** Dictionary namespace this plugin owns; every seat it fills declares it. */
|
|
|
+const NS = 'settings'
|
|
|
|
|
|
/** The seats this plugin fills for a loopback browser (slot name → expected component). */
|
|
|
const SEATS = [
|
|
|
@@ -25,56 +35,55 @@ const SEATS = [
|
|
|
['settings.section', GeneralSection],
|
|
|
] as const
|
|
|
|
|
|
-async function bench(isLoopback = true) {
|
|
|
- const ctx = new Context()
|
|
|
- await ctx.plugin(SlotRegistry).await()
|
|
|
- const locale = new LocaleRuntime(ctx)
|
|
|
- locale.setLocale('zh')
|
|
|
- ctx.provide('locale', locale)
|
|
|
- const settingsDescribe = vi.fn(() => Promise.resolve({
|
|
|
- ok: true as const,
|
|
|
- value: {
|
|
|
- writable: true,
|
|
|
- hasDocument: true,
|
|
|
- namespaces: [],
|
|
|
- },
|
|
|
- }))
|
|
|
- const settingsOpenDocument = vi.fn(() => Promise.resolve({
|
|
|
- ok: true as const, value: { opened: true as const },
|
|
|
- }))
|
|
|
- const remote = new TestRemote(ctx, {
|
|
|
- settings: { describe: settingsDescribe, openSettingsDocument: settingsOpenDocument },
|
|
|
- })
|
|
|
- // The fixed Host facts the shell reads its loopback-only action from.
|
|
|
- remote.$host = { home: undefined, isLoopback }
|
|
|
- ctx.provide('connection', {
|
|
|
- state: { getSnapshot: () => 'connected', subscribe: () => () => {} },
|
|
|
- reconnect: () => {},
|
|
|
- } as never)
|
|
|
- await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
|
|
|
- return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, settingsDescribe, settingsOpenDocument }
|
|
|
+/** One Host view of the locale preference, including its revision fence. */
|
|
|
+function localeView(preference: string, revision = 0): SettingsNamespaceView {
|
|
|
+ return {
|
|
|
+ ns: LOCALE_SETTINGS_NAMESPACE,
|
|
|
+ // The Remote wire serializes nested Schema values before the client rehydrates them.
|
|
|
+ schema: JSON.parse(JSON.stringify(LocaleSettingsSchema.toJSON())) as SettingsNamespaceView['schema'],
|
|
|
+ value: { preference },
|
|
|
+ applies: 'live',
|
|
|
+ secrets: [],
|
|
|
+ revision,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function client(mock: RemoteMock, start: () => Promise<TestClient>, hasDocument = false) {
|
|
|
+ const settings = mock.remote.settings
|
|
|
+ settings.describe.mockResolvedValue(ok({ writable: true, hasDocument, namespaces: [localeView('zh')] }))
|
|
|
+ const c = await start()
|
|
|
+ // The locale adopts the Host preference once the describe mirror holds the document.
|
|
|
+ await c.ctx.settingsScope.describe().ensure()
|
|
|
+ return { c, settings }
|
|
|
+}
|
|
|
+
|
|
|
+/** This plugin's rows in a seat: the list seats also carry feature-owned rows (the product's other sections and actions). */
|
|
|
+function ownEntries(c: TestClient, name: (typeof SEATS)[number][0]) {
|
|
|
+ return c.ctx.slots.entries(name).filter(entry => entry.locale === NS)
|
|
|
+}
|
|
|
+
|
|
|
+function generalEntry(c: TestClient) {
|
|
|
+ return ownEntries(c, 'settings.section').find(entry => entry.component === GeneralSection)!
|
|
|
}
|
|
|
|
|
|
-/** Declare the shell's six child slots the way ui-settings' entry does. */
|
|
|
-function declare(slots: SlotRegistry): () => void {
|
|
|
- return slots.register(
|
|
|
- {
|
|
|
- name: 'root',
|
|
|
- children: {
|
|
|
- '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 never,
|
|
|
- () => null,
|
|
|
- )
|
|
|
+function generalLabel(c: TestClient): string | undefined {
|
|
|
+ return resolveSlotLabel(generalEntry(c).options.label)
|
|
|
}
|
|
|
|
|
|
-function generalEntry(slots: SlotRegistry) {
|
|
|
- return slots.entries('settings.section').find(e => e.component === GeneralSection)
|
|
|
+function actionInjectedOf(c: TestClient): SettingsDocumentActionInjected {
|
|
|
+ const entry = ownEntries(c, 'settings.action')[0]!
|
|
|
+ return (entry.inject as unknown as () => SettingsDocumentActionInjected)()
|
|
|
+}
|
|
|
+
|
|
|
+function expectSeated(c: TestClient): void {
|
|
|
+ for (const [name, component] of SEATS) {
|
|
|
+ expect(ownEntries(c, name).map(entry => entry.component)).toEqual([component])
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** The page authority the `connection` plugin classifies at apply, reconfigured through the jsdom instance vitest exposes. */
|
|
|
+function setPageUrl(url: string): void {
|
|
|
+ (globalThis as unknown as { jsdom: { reconfigure(settings: { url: string }): void } }).jsdom.reconfigure({ url })
|
|
|
}
|
|
|
|
|
|
describe('ui-settings-general apply', () => {
|
|
|
@@ -82,138 +91,144 @@ describe('ui-settings-general apply', () => {
|
|
|
expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'remote.settings', 'settingsScope'])
|
|
|
})
|
|
|
|
|
|
- it('fills all five seats for declarations before or after apply', async () => {
|
|
|
- const before = await bench()
|
|
|
- declare(before.slots)
|
|
|
- await before.ctx.plugin({ inject: [...inject], apply }).await()
|
|
|
- for (const [name, component] of SEATS) {
|
|
|
- expect(before.slots.entries(name)[0]!.component).toBe(component)
|
|
|
- }
|
|
|
- const entry = generalEntry(before.slots)!
|
|
|
+ it('fills the five seats of the shell it declares, with the locale-following General label', async ({ mock, start }) => {
|
|
|
+ const { c } = await client(mock, start)
|
|
|
+ expect(c.ctx.locale.getSnapshot().active).toBe('zh')
|
|
|
+ expectSeated(c)
|
|
|
+ const entry = generalEntry(c)
|
|
|
expect(entry.options).toMatchObject({ id: 'general', order: 0 })
|
|
|
// The nav label is a locale-following thunk; owners resolve at read time.
|
|
|
- expect(resolveSlotLabel(entry.options.label)).toBe('通用设置')
|
|
|
- expect(before.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
|
|
|
- expect(before.slots.entries('settings.general.item')).toEqual([])
|
|
|
- // The onboarding hole stays declared for feature-owned steps; this plugin
|
|
|
- // no longer seats one.
|
|
|
- expect(before.slots.entries('settings.onboarding')).toEqual([])
|
|
|
- const action = before.slots.entries('settings.action')[0]!
|
|
|
- const actionInjected = (action.inject as unknown as () => SettingsDocumentActionInjected)()
|
|
|
- expect(actionInjected.controller.store.getSnapshot().status).toBe('idle')
|
|
|
- expect(actionInjected.hooks.snapshot).toBe(actionInjected.controller.store)
|
|
|
- // Copy rides the standard locale seat: every seat declares the namespace.
|
|
|
- for (const [name] of SEATS) {
|
|
|
- expect(before.slots.entries(name)[0]!.locale).toBe('settings')
|
|
|
- }
|
|
|
- const after = await bench()
|
|
|
- await after.ctx.plugin({ inject: [...inject], apply }).await()
|
|
|
- for (const [name] of SEATS) expect(after.slots.entries(name)).toHaveLength(0)
|
|
|
- declare(after.slots)
|
|
|
- await Promise.resolve()
|
|
|
+ expect(generalLabel(c)).toBe('通用设置')
|
|
|
+ expect(c.ctx.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
|
|
|
+ // The General items and the onboarding steps are feature-owned rows; this plugin seats none of its own.
|
|
|
+ expect(c.ctx.slots.entries('settings.general.item').filter(row => row.locale === NS)).toEqual([])
|
|
|
+ expect(c.ctx.slots.entries('settings.onboarding').filter(row => row.locale === NS)).toEqual([])
|
|
|
+ const { controller, hooks } = actionInjectedOf(c)
|
|
|
+ expect(controller.store.getSnapshot().status).toBe('idle')
|
|
|
+ expect(hooks.snapshot).toBe(controller.store)
|
|
|
+ // Copy rides the standard locale seat: every row this plugin seats declares the namespace.
|
|
|
for (const [name, component] of SEATS) {
|
|
|
- expect(after.slots.entries(name)[0]!.component).toBe(component)
|
|
|
- // The self-inflicted ledger notifications hit the duplicate guard.
|
|
|
- expect(after.slots.entries(name)).toHaveLength(1)
|
|
|
+ expect(c.ctx.slots.entries(name).find(row => row.component === component)!.locale).toBe(NS)
|
|
|
}
|
|
|
+ }, COLD_BOOT_TIMEOUT_MS)
|
|
|
+
|
|
|
+ it('registers the zh/en settings dictionaries and frees the seats when its row unloads', async ({ mock, start }) => {
|
|
|
+ const { c, settings } = await client(mock, start)
|
|
|
+ const english = localeView('en', 1)
|
|
|
+ settings.mutate.mockResolvedValueOnce(ok(english))
|
|
|
+ const t = c.ctx.locale.bind(NS)
|
|
|
+ expect(t('title')).toBe('设置')
|
|
|
+ expect(t('connection.error')).toBe('连接异常')
|
|
|
+ expect(t('connection.connecting')).toBe('自动重连中')
|
|
|
+ expect(t('connection.connected')).toBe('连接成功')
|
|
|
+ c.ctx.locale.setLocale('en')
|
|
|
+ expect(t('close')).toBe('Close')
|
|
|
+ expect(t('connection.reconnect')).toBe('Disconnected, reconnect now')
|
|
|
+ expect(t('connection.connecting')).toBe('Reconnecting')
|
|
|
await vi.waitFor(() => {
|
|
|
- expect(after.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
|
|
|
+ expect(settings.mutate.mock.calls).toEqual([
|
|
|
+ [LOCALE_SETTINGS_NAMESPACE, [{ op: 'set', path: ['preference'], value: 'en' }], 0],
|
|
|
+ ])
|
|
|
+ expect(c.ctx.settingsScope.describe().getSnapshot().view?.namespaces).toEqual([english])
|
|
|
})
|
|
|
- })
|
|
|
-
|
|
|
- it('registers the zh/en settings dictionaries and frees the seats on teardown', async () => {
|
|
|
- const b = await bench()
|
|
|
- declare(b.slots)
|
|
|
- const fiber = b.ctx.plugin({ inject: [...inject], apply })
|
|
|
- await fiber.await()
|
|
|
- expect(b.locale.bind('settings')('title')).toBe('设置')
|
|
|
- expect(b.locale.bind('settings')('connection.error')).toBe('连接异常')
|
|
|
- expect(b.locale.bind('settings')('connection.connecting')).toBe('自动重连中')
|
|
|
- expect(b.locale.bind('settings')('connection.connected')).toBe('连接成功')
|
|
|
- b.locale.setLocale('en')
|
|
|
- expect(b.locale.bind('settings')('close')).toBe('Close')
|
|
|
- expect(b.locale.bind('settings')('connection.reconnect')).toBe('Disconnected, reconnect now')
|
|
|
- expect(b.locale.bind('settings')('connection.connecting')).toBe('Reconnecting')
|
|
|
- b.locale.setLocale('zh')
|
|
|
- await fiber.dispose()
|
|
|
+ await c.unload(SELF)
|
|
|
+ await c.flush()
|
|
|
// The (ns, locale) seats are free again — the dictionary disposer ran.
|
|
|
- expect(() => b.locale.register('settings', 'zh', {})).not.toThrow()
|
|
|
- expect(() => b.locale.register('settings', 'en', {})).not.toThrow()
|
|
|
+ expect(() => { c.ctx.locale.register(NS, 'zh', {})() }).not.toThrow()
|
|
|
+ expect(() => { c.ctx.locale.register(NS, 'en', {})() }).not.toThrow()
|
|
|
})
|
|
|
|
|
|
- it('the nav label thunk follows the active locale without re-registration', async () => {
|
|
|
- const b = await bench()
|
|
|
- declare(b.slots)
|
|
|
- await b.ctx.plugin({ inject: [...inject], apply }).await()
|
|
|
- const zhVersions = SEATS.map(([name]) => b.slots.getVersion(name))
|
|
|
- b.locale.setLocale('en')
|
|
|
+ it('the nav label thunk follows the active locale without re-registration', async ({ mock, start }) => {
|
|
|
+ const { c, settings } = await client(mock, start)
|
|
|
+ const english = localeView('en', 1)
|
|
|
+ const chinese = localeView('zh', 2)
|
|
|
+ settings.mutate.mockResolvedValueOnce(ok(english)).mockResolvedValueOnce(ok(chinese))
|
|
|
+ const zhVersions = SEATS.map(([name]) => c.ctx.slots.getVersion(name))
|
|
|
+ c.ctx.locale.setLocale('en')
|
|
|
// No ledger churn: freshness rides the thunk (and the renderer's locale
|
|
|
// subscription), not re-registration.
|
|
|
SEATS.forEach(([name], i) => {
|
|
|
- expect(b.slots.getVersion(name)).toBe(zhVersions[i]!)
|
|
|
- expect(b.slots.entries(name)).toHaveLength(1)
|
|
|
+ expect(c.ctx.slots.getVersion(name)).toBe(zhVersions[i]!)
|
|
|
+ expect(ownEntries(c, name)).toHaveLength(1)
|
|
|
+ })
|
|
|
+ expect(generalLabel(c)).toBe('General')
|
|
|
+ await vi.waitFor(() => {
|
|
|
+ expect(c.ctx.settingsScope.describe().getSnapshot().view?.namespaces).toEqual([english])
|
|
|
+ })
|
|
|
+ c.ctx.locale.setLocale('zh')
|
|
|
+ expect(generalLabel(c)).toBe('通用设置')
|
|
|
+ await vi.waitFor(() => {
|
|
|
+ expect(settings.mutate.mock.calls).toEqual([
|
|
|
+ [LOCALE_SETTINGS_NAMESPACE, [{ op: 'set', path: ['preference'], value: 'en' }], 0],
|
|
|
+ [LOCALE_SETTINGS_NAMESPACE, [{ op: 'set', path: ['preference'], value: 'zh' }], 1],
|
|
|
+ ])
|
|
|
+ expect(c.ctx.settingsScope.describe().getSnapshot().view?.namespaces).toEqual([chinese])
|
|
|
})
|
|
|
- expect(resolveSlotLabel(generalEntry(b.slots)!.options.label)).toBe('General')
|
|
|
- b.locale.setLocale('zh')
|
|
|
- expect(resolveSlotLabel(generalEntry(b.slots)!.options.label)).toBe('通用设置')
|
|
|
})
|
|
|
|
|
|
- it('reads availability from the shared mirror and follows its reconnect refresh', async () => {
|
|
|
- const b = await bench()
|
|
|
- declare(b.slots)
|
|
|
- await b.ctx.plugin({ inject: [...inject], apply }).await()
|
|
|
- const entry = b.slots.entries('settings.action')[0]!
|
|
|
- const { controller } = (entry.inject as unknown as () => SettingsDocumentActionInjected)()
|
|
|
- // The mirror read once at its own boot; the action's load adds no read.
|
|
|
- await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledOnce() })
|
|
|
+ it('reads availability from the shared mirror and follows its reconnect refresh', async ({ mock, start }) => {
|
|
|
+ const { c } = await client(mock, start, true)
|
|
|
+ const { controller } = actionInjectedOf(c)
|
|
|
+ // Boot reads the document twice: the mirror's own `ensure` at apply, then
|
|
|
+ // the `connection/reset` of the first connection. The action's load adds none.
|
|
|
+ expect(c.mock.log.calls('settings/describe')).toHaveLength(2)
|
|
|
await controller.load()
|
|
|
- expect(b.settingsDescribe).toHaveBeenCalledOnce()
|
|
|
+ expect(c.mock.log.calls('settings/describe')).toHaveLength(2)
|
|
|
expect(controller.store.getSnapshot().status).toBe('ready')
|
|
|
- b.ctx.emit('connection/reset')
|
|
|
- await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(2) })
|
|
|
+ c.connection.reconnect()
|
|
|
+ await c.mock.streams.opened('$events', 2)
|
|
|
+ await vi.waitFor(() => { expect(c.mock.log.calls('settings/describe')).toHaveLength(3) })
|
|
|
})
|
|
|
|
|
|
- it('withholds the Host document action off-loopback', async () => {
|
|
|
- const b = await bench(false)
|
|
|
- declare(b.slots)
|
|
|
- const fiber = b.ctx.plugin({ inject: [...inject], apply })
|
|
|
- await fiber.await()
|
|
|
- expect(b.slots.entries('settings.action')).toEqual([])
|
|
|
- expect(b.settingsDescribe).not.toHaveBeenCalled()
|
|
|
- await fiber.dispose()
|
|
|
- for (const [name] of SEATS) expect(b.slots.entries(name)).toEqual([])
|
|
|
+ it('withholds the Host document action off-loopback', async ({ mock, start }) => {
|
|
|
+ const loopbackUrl = location.href
|
|
|
+ setPageUrl('http://198.51.100.7:3000/')
|
|
|
+ onTestFinished(() => { setPageUrl(loopbackUrl) })
|
|
|
+ const { c } = await client(mock, start)
|
|
|
+ expect(c.connection.isLoopback).toBe(false)
|
|
|
+ expect(ownEntries(c, 'settings.action')).toEqual([])
|
|
|
+ // Off-loopback settings stay process-local: no describe read, so the browser language stands.
|
|
|
+ expect(c.mock.log.calls('settings/describe')).toEqual([])
|
|
|
+ expect(c.ctx.locale.getSnapshot().active).toBe('en')
|
|
|
+ await c.unload(SELF)
|
|
|
+ await c.flush()
|
|
|
+ for (const [name] of SEATS) expect(ownEntries(c, name)).toEqual([])
|
|
|
})
|
|
|
|
|
|
- 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)
|
|
|
- await b.ctx.plugin({ inject: [...inject], apply }).await()
|
|
|
- // Declarer unload: the cascade removes every seat entry and the item
|
|
|
- // declaration while our local disposers go stale.
|
|
|
- redeclare()
|
|
|
- for (const [name] of SEATS) expect(b.slots.entries(name)).toHaveLength(0)
|
|
|
- expect(b.slots.spec('settings.general.item')).toBeUndefined()
|
|
|
- declare(b.slots)
|
|
|
- await Promise.resolve()
|
|
|
- for (const [name, component] of SEATS) {
|
|
|
- expect(b.slots.entries(name)[0]!.component).toBe(component)
|
|
|
- }
|
|
|
- expect(b.slots.entries('settings.general.item')).toEqual([])
|
|
|
- expect(b.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
|
|
|
+ it('re-registers after a Loader rebuild of the declaring chain (stale disposers must not block)', async ({ mock, start }) => {
|
|
|
+ const { c, settings } = await client(mock, start)
|
|
|
+ const before = SEATS.map(([name]) => ownEntries(c, name)[0])
|
|
|
+ await c.reload(SIDEBAR)
|
|
|
+ await c.flush()
|
|
|
+ expectSeated(c)
|
|
|
+ SEATS.forEach(([name], index) => {
|
|
|
+ expect(ownEntries(c, name)[0]).not.toBe(before[index])
|
|
|
+ })
|
|
|
+ expect(c.ctx.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
|
|
|
+ expect(c.ctx.slots.entries('settings.general.item').filter(row => row.locale === NS)).toEqual([])
|
|
|
// The recovered registrations still ride the locale path.
|
|
|
- b.locale.setLocale('en')
|
|
|
- expect(resolveSlotLabel(generalEntry(b.slots)!.options.label)).toBe('General')
|
|
|
- b.locale.setLocale('zh')
|
|
|
+ const english = localeView('en', 1)
|
|
|
+ const chinese = localeView('zh', 2)
|
|
|
+ settings.mutate.mockResolvedValueOnce(ok(english)).mockResolvedValueOnce(ok(chinese))
|
|
|
+ c.ctx.locale.setLocale('en')
|
|
|
+ expect(generalLabel(c)).toBe('General')
|
|
|
+ c.ctx.locale.setLocale('zh')
|
|
|
+ expect(generalLabel(c)).toBe('通用设置')
|
|
|
+ await vi.waitFor(() => {
|
|
|
+ expect(settings.mutate.mock.calls).toEqual([
|
|
|
+ [LOCALE_SETTINGS_NAMESPACE, [{ op: 'set', path: ['preference'], value: 'en' }], 0],
|
|
|
+ [LOCALE_SETTINGS_NAMESPACE, [{ op: 'set', path: ['preference'], value: 'zh' }], 1],
|
|
|
+ ])
|
|
|
+ expect(c.ctx.settingsScope.describe().getSnapshot().view?.namespaces).toEqual([chinese])
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
- it('removes every seat and the item declaration on teardown', async () => {
|
|
|
- const b = await bench()
|
|
|
- declare(b.slots)
|
|
|
- const fiber = b.ctx.plugin({ inject: [...inject], apply })
|
|
|
- await fiber.await()
|
|
|
- expect(b.slots.spec('settings.general.item')).toBeDefined()
|
|
|
- await fiber.dispose()
|
|
|
- for (const [name] of SEATS) expect(b.slots.entries(name)).toHaveLength(0)
|
|
|
- expect(b.slots.spec('settings.general.item')).toBeUndefined()
|
|
|
+ it('removes every seat and the item declaration when its row unloads', async ({ mock, start }) => {
|
|
|
+ const { c } = await client(mock, start)
|
|
|
+ expect(c.ctx.slots.spec('settings.general.item')).toBeDefined()
|
|
|
+ await c.unload(SELF)
|
|
|
+ await c.flush()
|
|
|
+ for (const [name] of SEATS) expect(ownEntries(c, name)).toHaveLength(0)
|
|
|
+ expect(c.ctx.slots.spec('settings.general.item')).toBeUndefined()
|
|
|
})
|
|
|
})
|