From a3ee5dd8a0c2ba5ddc850b485eb07f88cb11fff1 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sun, 26 Jul 2026 02:54:29 +0800 Subject: [PATCH] docs(gui): regenerate graphs and sync the note's English pair Generated docs follow the ui-models rename and the removed ui-settings-general package; the English note side picks up the feature-owner self-registration doctrine. --- ...-25-client-settings-locale-theme.i18n.yaml | 4 +- ...2026-07-25-client-settings-locale-theme.md | 2 +- docs/config-catalog.md | 3 +- docs/event-producer-consumer.md | 4 +- docs/module-graph.md | 41 +++++----- .../client/locale/tests/language-row.spec.tsx | 82 +++++++++++++++++++ .../locale/tests/settings-store.spec.ts | 30 +++++++ .../client/ui-settings/tests/apply.spec.ts | 6 +- .../tests/general-section.spec.tsx | 4 + .../ui-theme/tests/appearance-row.spec.tsx | 75 +++++++++++++++++ .../ui-theme/tests/settings-store.spec.ts | 28 +++++++ 11 files changed, 250 insertions(+), 29 deletions(-) create mode 100644 packages/client/locale/tests/language-row.spec.tsx create mode 100644 packages/client/locale/tests/settings-store.spec.ts create mode 100644 packages/client/ui-theme/tests/appearance-row.spec.tsx create mode 100644 packages/client/ui-theme/tests/settings-store.spec.ts diff --git a/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.i18n.yaml index 8583642403..39d7377c54 100644 --- a/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.i18n.yaml +++ b/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-25-client-settings-locale-theme.md: e1245a9e1fac82fb0feb84af7a59945b17fa1daf -2026-07-25-client-settings-locale-theme.zh.md: 195b2e5ffa3556dd1b8bf2dd6ec8ae84115fbdb2 +2026-07-25-client-settings-locale-theme.md: 658e6bd3c2da39a98e476c60f10f3f51ad82e5ea +2026-07-25-client-settings-locale-theme.zh.md: dfe93a0ca1f53380c653886c73fe0c32b08d8443 diff --git a/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.md b/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.md index 0dfcb4ea90..658e6bd3c2 100644 --- a/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.md +++ b/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.md @@ -119,4 +119,4 @@ Locale ships with 中文 and English built in; `setLocale`/`setTheme` are the on ## Risks -The apply order of slot declarations and contributions is not fixed, so every new section must keep declaration-aware registration and idempotence guards. Service events may fire before a section's first render, so both the General store's init and the controller attach must align to the current snapshot from the getters. Layout must clean up the global attributes it set on unmount, and ThemeService must remove its matchMedia listener on dispose, so nothing lingers after HMR. +The apply order of slot declarations and contributions is not fixed, so every section/item registrant must keep declaration-aware registration and judge presence by the ledger, not by a local disposer. Service events may fire before a row's first render, so both a feature row store's init and the inject attach must align to the current snapshot from the getter. The duplicated merge copies of `settings.general.item` (locale, ui-theme) must stay verbatim-identical to the ui-settings canonical home — any drift means changing all three together. Layout must clean up the global attributes it set on unmount, and ThemeService must remove its matchMedia listener on dispose, so nothing lingers after HMR. diff --git a/docs/config-catalog.md b/docs/config-catalog.md index ede904e1a5..1940811d35 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2022,10 +2022,9 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-client-runtime` ([`packages/client/runtime/src/index.ts`](../packages/client/runtime/src/index.ts)) - `@deepseek-ai/dsh-client-ui-conversation` ([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts)) - `@deepseek-ai/dsh-client-ui-layout` ([`packages/client/ui-layout/src/index.ts`](../packages/client/ui-layout/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-models` ([`packages/client/ui-models/src/index.ts`](../packages/client/ui-models/src/index.ts)) - `@deepseek-ai/dsh-client-ui-question` — requires `tools` · `userInteraction` ([`packages/client/ui-question/src/index.ts`](../packages/client/ui-question/src/index.ts)) - `@deepseek-ai/dsh-client-ui-settings` ([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts)) -- `@deepseek-ai/dsh-client-ui-settings-general` ([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts)) -- `@deepseek-ai/dsh-client-ui-settings-models` ([`packages/client/ui-settings-models/src/index.ts`](../packages/client/ui-settings-models/src/index.ts)) - `@deepseek-ai/dsh-client-ui-sidebar` ([`packages/client/ui-sidebar/src/index.ts`](../packages/client/ui-sidebar/src/index.ts)) - `@deepseek-ai/dsh-client-ui-theme` ([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts)) - `@deepseek-ai/dsh-client-ui-trajectory` ([`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts)) diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 2c1e85fb3c..53cd56f4cf 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -63,8 +63,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `internal/dispatch` | - | [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`workflow`](../packages/workflow/workflow) | | `internal/plugin` | - | `hmr`, `modules`, `webserver` | | `internal/status` | - | [`agent`](../packages/core/agent) | -| `locale/change` | `locale` (`emit`) | `ui-settings-general`, `ui-settings-models` | +| `locale/change` | `locale` (`emit`) | `locale`, `ui-models`, `ui-settings` | | `slots/changed` | `runtime` (`emit`) | - | -| `theme/change` | `ui-theme` (`emit`) | `ui-layout`, `ui-settings-general` | +| `theme/change` | `ui-theme` (`emit`) | `ui-layout`, `ui-theme` | Maintenance mode: generated: Cordis event declarations and producer/listener edges are resolved from the repository TypeScript Program. diff --git a/docs/module-graph.md b/docs/module-graph.md index c47bd92d83..4903e432cd 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -141,11 +141,10 @@ flowchart TD pkg_client_runtime["client-runtime"] pkg_client_ui_conversation["client-ui-conversation"] pkg_client_ui_layout["client-ui-layout"] + pkg_client_ui_models["client-ui-models"] pkg_client_ui_primitives["client-ui-primitives"] pkg_client_ui_question["client-ui-question"] pkg_client_ui_settings["client-ui-settings"] - pkg_client_ui_settings_general["client-ui-settings-general"] - pkg_client_ui_settings_models["client-ui-settings-models"] pkg_client_ui_sidebar["client-ui-sidebar"] pkg_client_ui_slots["client-ui-slots"] pkg_client_ui_theme["client-ui-theme"] @@ -228,13 +227,11 @@ flowchart TD pkg_subagent_subprocess --> pkg_invariants pkg_acp_snapshot --> pkg_invariants pkg_loader_smoke --> pkg_invariants - pkg_client_locale --> pkg_invariants pkg_client_modules --> pkg_invariants pkg_client_runtime --> pkg_invariants pkg_client_ui_primitives --> pkg_invariants pkg_client_ui_question --> pkg_invariants pkg_client_ui_slots --> pkg_invariants - pkg_client_ui_theme --> pkg_invariants pkg_client_ui_trajectory --> pkg_invariants pkg_client_web --> pkg_invariants pkg_client_web_react --> pkg_invariants @@ -250,25 +247,21 @@ flowchart TD pkg_client_hmr --> pkg_client_modules pkg_client_hmr --> pkg_host_webserver pkg_client_hmr --> pkg_invariants + pkg_client_locale --> pkg_client_runtime + pkg_client_locale --> pkg_client_ui_primitives + pkg_client_locale --> pkg_client_ui_slots + pkg_client_locale --> pkg_invariants pkg_client_ui_conversation --> pkg_client_runtime pkg_client_ui_conversation --> pkg_client_ui_primitives pkg_client_ui_conversation --> pkg_client_ui_slots pkg_client_ui_conversation --> pkg_invariants - pkg_client_ui_layout --> pkg_client_runtime - pkg_client_ui_layout --> pkg_client_ui_slots - pkg_client_ui_layout --> pkg_client_ui_theme - pkg_client_ui_layout --> pkg_invariants + pkg_client_ui_models --> pkg_client_runtime + pkg_client_ui_models --> pkg_client_ui_slots + pkg_client_ui_models --> pkg_invariants pkg_client_ui_settings --> pkg_client_runtime pkg_client_ui_settings --> pkg_client_ui_primitives pkg_client_ui_settings --> pkg_client_ui_slots pkg_client_ui_settings --> pkg_invariants - pkg_client_ui_settings_general --> pkg_client_runtime - pkg_client_ui_settings_general --> pkg_client_ui_primitives - pkg_client_ui_settings_general --> pkg_client_ui_slots - pkg_client_ui_settings_general --> pkg_invariants - pkg_client_ui_settings_models --> pkg_client_runtime - pkg_client_ui_settings_models --> pkg_client_ui_slots - pkg_client_ui_settings_models --> pkg_invariants pkg_client_ui_sidebar --> pkg_client_runtime pkg_client_ui_sidebar --> pkg_client_ui_primitives pkg_client_ui_sidebar --> pkg_client_ui_slots @@ -303,6 +296,11 @@ flowchart TD pkg_system_prompt --> pkg_scope pkg_web --> pkg_invariants pkg_web --> pkg_llm + pkg_client_ui_theme --> pkg_client_locale + pkg_client_ui_theme --> pkg_client_runtime + pkg_client_ui_theme --> pkg_client_ui_primitives + pkg_client_ui_theme --> pkg_client_ui_slots + pkg_client_ui_theme --> pkg_invariants pkg_lsp --> pkg_brand pkg_lsp --> pkg_invariants pkg_lsp --> pkg_llm @@ -355,6 +353,10 @@ flowchart TD pkg_app_boot --> pkg_invariants pkg_app_boot --> pkg_paths pkg_app_boot --> pkg_system_prompt + pkg_client_ui_layout --> pkg_client_runtime + pkg_client_ui_layout --> pkg_client_ui_slots + pkg_client_ui_layout --> pkg_client_ui_theme + pkg_client_ui_layout --> pkg_invariants pkg_code_runtime_worker --> pkg_code_runtime pkg_code_runtime_worker --> pkg_invariants pkg_code_runtime_worker --> pkg_session @@ -813,13 +815,11 @@ flowchart TD | [`subagent-subprocess`](../packages/subagent/subagent-subprocess) | `subagent` | [`invariants`](../packages/support/invariants) | | [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants) | | [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`invariants`](../packages/support/invariants) | -| [`client-locale`](../packages/client/locale) | `client` | [`invariants`](../packages/support/invariants) | | [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/support/invariants) | | [`client-runtime`](../packages/client/runtime) | `client` | [`invariants`](../packages/support/invariants) | | [`client-ui-primitives`](../packages/client/ui-primitives) | `client` | [`invariants`](../packages/support/invariants) | | [`client-ui-question`](../packages/client/ui-question) | `client` | [`invariants`](../packages/support/invariants) | | [`client-ui-slots`](../packages/client/ui-slots) | `client` | [`invariants`](../packages/support/invariants) | -| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`invariants`](../packages/support/invariants) | | [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`invariants`](../packages/support/invariants) | | [`client-web`](../packages/client/web) | `client` | [`invariants`](../packages/support/invariants) | | [`client-web-react`](../packages/client/web-react) | `client` | [`invariants`](../packages/support/invariants) | @@ -831,11 +831,10 @@ flowchart TD | [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | | [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | | [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | +| [`client-locale`](../packages/client/locale) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) | +| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`helper`](../packages/sdk/helper) | `sdk` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | @@ -848,6 +847,7 @@ flowchart TD | [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | | [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | | [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | +| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | | [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | | [`token-meter`](../packages/llm/token-meter) | `llm` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | @@ -865,6 +865,7 @@ flowchart TD | [`session-title`](../packages/session-title/session-title) | `session-title` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`llm-replay`](../packages/support/llm-replay) | `support` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`app-boot`](../packages/ui/app-boot) | `ui` | [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) | +| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) | | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`timeout`](../packages/util/timeout) | | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | diff --git a/packages/client/locale/tests/language-row.spec.tsx b/packages/client/locale/tests/language-row.spec.tsx new file mode 100644 index 0000000000..af33038970 --- /dev/null +++ b/packages/client/locale/tests/language-row.spec.tsx @@ -0,0 +1,82 @@ +// @vitest-environment jsdom +/** LanguageRow behavior: selector pill shows the active locale, the menu + * opens/closes, and selection drives setLocale. */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import { createSnapshotStore, type SessionListState, type WorkspaceListState } from '@deepseek-ai/dsh-client-runtime/client' +import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react' +import { LanguageRow } from '../src/client/LanguageRow.tsx' +import type { LanguageRowComponentProps } from '../src/client/LanguageRow.tsx' +import { createLanguageRowStore } from '../src/client/settings-store.ts' + +afterEach(cleanup) + +const OPTIONS = [{ id: 'zh', label: '中文' }, { id: 'en', label: 'English' }] + +/** Empty global standard-kit hooks (the row reads neither). */ +function emptySessions() { + const store = createSnapshotStore( + { ids: [], byId: {}, current: undefined, intent: undefined, phase: 'ready' }) + return bindSnapshotSelector(store) +} +function emptyWorkspaces() { + const store = createSnapshotStore({ + items: [], intent: undefined, state: 'idle', phase: 'ready', error: null, + baselinesReady: true, recentWorkspaceId: undefined, + }) + return bindSnapshotSelector(store) +} + +function mount(active = 'en') { + // Real store instance — the sanctioned zero-machinery path for tests. + const store = createLanguageRowStore().create() + store.actions.sync(active, OPTIONS, 0) + const setLocale = vi.fn() + const props: LanguageRowComponentProps = { + useSessions: emptySessions(), + useWorkspaces: emptyWorkspaces(), + useStore: bindSnapshotSelector(store), + actions: store.actions, + t: (key: string) => key === 'language.title' ? 'Language' : key, + setLocale, + } + render() + return { store, setLocale } +} + +describe('LanguageRow', () => { + it('shows the title and the active locale label on the selector pill', () => { + mount('en') + expect(screen.getByText('Language')).toBeDefined() + const trigger = screen.getByRole('button', { name: /English/ }) + expect(trigger.getAttribute('aria-expanded')).toBe('false') + }) + + it('opens the menu, selects a locale, and closes', () => { + const b = mount('en') + const trigger = screen.getByRole('button', { name: /English/ }) + fireEvent.click(trigger) + expect(trigger.getAttribute('aria-expanded')).toBe('true') + fireEvent.click(screen.getByRole('menuitem', { name: '中文' })) + expect(b.setLocale).toHaveBeenCalledWith('zh') + expect(trigger.getAttribute('aria-expanded')).toBe('false') + expect(screen.queryByRole('menuitem', { name: '中文' })).toBeNull() + }) + + it('closes on outside pointerdown without selecting', () => { + const b = mount('en') + fireEvent.click(screen.getByRole('button', { name: /English/ })) + expect(screen.getByRole('menuitem', { name: '中文' })).toBeDefined() + fireEvent.pointerDown(document.body) + expect(screen.queryByRole('menuitem', { name: '中文' })).toBeNull() + expect(b.setLocale).not.toHaveBeenCalled() + }) + + it('follows store changes; an unknown active id falls back to the id itself', () => { + const b = mount('en') + act(() => { b.store.actions.sync('zh', OPTIONS, 1) }) + expect(screen.getByRole('button', { name: /中文/ })).toBeDefined() + act(() => { b.store.actions.sync('fr', OPTIONS, 2) }) + expect(screen.getByRole('button', { name: /fr/ })).toBeDefined() + }) +}) diff --git a/packages/client/locale/tests/settings-store.spec.ts b/packages/client/locale/tests/settings-store.spec.ts new file mode 100644 index 0000000000..90e9487a96 --- /dev/null +++ b/packages/client/locale/tests/settings-store.spec.ts @@ -0,0 +1,30 @@ +/** Language row store: snapshot-mirror action and the revision guard. */ +import { describe, expect, it } from 'vitest' +import { createLanguageRowStore } from '../src/client/settings-store.ts' + +const OPTIONS = [{ id: 'zh', label: '中文' }, { id: 'en', label: 'English' }] + +describe('createLanguageRowStore', () => { + it('init shape: empty mirror with revision at -1', () => { + const store = createLanguageRowStore().create() + expect(store.getSnapshot()).toEqual({ active: '', options: [], revision: -1 }) + }) + + it('sync mirrors the snapshot and advances the revision', () => { + const store = createLanguageRowStore().create() + store.actions.sync('zh', OPTIONS, 0) + expect(store.getSnapshot()).toEqual({ active: 'zh', options: OPTIONS, revision: 0 }) + store.actions.sync('en', OPTIONS, 1) + expect(store.getSnapshot().active).toBe('en') + expect(store.getSnapshot().revision).toBe(1) + }) + + it('revision guard drops stale and duplicate writes', () => { + const store = createLanguageRowStore().create() + store.actions.sync('en', OPTIONS, 5) + store.actions.sync('zh', OPTIONS, 4) + store.actions.sync('zh', OPTIONS, 5) + expect(store.getSnapshot().active).toBe('en') + expect(store.getSnapshot().revision).toBe(5) + }) +}) diff --git a/packages/client/ui-settings/tests/apply.spec.ts b/packages/client/ui-settings/tests/apply.spec.ts index 95684404a1..2346012553 100644 --- a/packages/client/ui-settings/tests/apply.spec.ts +++ b/packages/client/ui-settings/tests/apply.spec.ts @@ -82,10 +82,12 @@ describe('ui-settings apply', () => { // empty once apply settles. expect(injected.sections()).toEqual([{ id: 'general', order: 0, label: '通用设置' }]) b.slots.register({ name: 'settings.section', id: 'z', order: 20, label: 'Z' } as never, () => null) - b.slots.register({ name: 'settings.section', id: 'a', order: 5 } as never, () => null) + // No order and no label: both projection defaults apply (order 0 ties + // keep registration sequence, so 'a' lands after the General entry). + b.slots.register({ name: 'settings.section', id: 'a' } as never, () => null) expect(injected.sections()).toEqual([ { id: 'general', order: 0, label: '通用设置' }, - { id: 'a', order: 5, label: '' }, + { id: 'a', order: 0, label: '' }, { id: 'z', order: 20, label: 'Z' }, ]) expect(injected.sectionsVersion()).toBe(b.slots.getVersion('settings.section')) diff --git a/packages/client/ui-settings/tests/general-section.spec.tsx b/packages/client/ui-settings/tests/general-section.spec.tsx index a944dd8b15..ce09aabf93 100644 --- a/packages/client/ui-settings/tests/general-section.spec.tsx +++ b/packages/client/ui-settings/tests/general-section.spec.tsx @@ -11,7 +11,11 @@ function mount() { const renderSlot = vi.fn( ((key: string) =>
) as GeneralSectionComponentProps['renderSlot'], ) + // Global standard kit stubs: the section consumes neither hook. + const unusedHook = (() => { throw new Error('unused by GeneralSection') }) as never const props: GeneralSectionComponentProps = { + useSessions: unusedHook, + useWorkspaces: unusedHook, t: (key) => en[key] ?? key, renderSlot, } diff --git a/packages/client/ui-theme/tests/appearance-row.spec.tsx b/packages/client/ui-theme/tests/appearance-row.spec.tsx new file mode 100644 index 0000000000..4782b674a8 --- /dev/null +++ b/packages/client/ui-theme/tests/appearance-row.spec.tsx @@ -0,0 +1,75 @@ +// @vitest-environment jsdom +/** AppearanceRow behavior: three cubes, selection follows the persisted + * preference, clicks drive setTheme. */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import { createSnapshotStore, type SessionListState, type WorkspaceListState } from '@deepseek-ai/dsh-client-runtime/client' +import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react' +import { AppearanceRow } from '../src/client/AppearanceRow.tsx' +import type { AppearanceRowComponentProps } from '../src/client/AppearanceRow.tsx' +import { createAppearanceRowStore } from '../src/client/settings-store.ts' +import type { ThemePreference } from '../src/client/index.ts' + +afterEach(cleanup) + +const COPY: Record = { + 'appearance.title': 'Appearance', + 'appearance.light': 'Light', + 'appearance.dark': 'Dark', + 'appearance.system': 'System', +} + +/** Empty global standard-kit hooks (the row reads neither). */ +function emptySessions() { + const store = createSnapshotStore( + { ids: [], byId: {}, current: undefined, intent: undefined, phase: 'ready' }) + return bindSnapshotSelector(store) +} +function emptyWorkspaces() { + const store = createSnapshotStore({ + items: [], intent: undefined, state: 'idle', phase: 'ready', error: null, + baselinesReady: true, recentWorkspaceId: undefined, + }) + return bindSnapshotSelector(store) +} + +function mount(preference: ThemePreference = 'system') { + // Real store instance — the sanctioned zero-machinery path for tests. + const store = createAppearanceRowStore().create() + store.actions.sync(preference, 0) + const setTheme = vi.fn() + const props: AppearanceRowComponentProps = { + useSessions: emptySessions(), + useWorkspaces: emptyWorkspaces(), + useStore: bindSnapshotSelector(store), + actions: store.actions, + t: (key: string) => COPY[key] ?? key, + setTheme, + } + render() + return { store, setTheme } +} + +const pressed = (name: RegExp): string | null => + screen.getByRole('button', { name }).getAttribute('aria-pressed') + +describe('AppearanceRow', () => { + it('renders the title and three cubes with the preference cube selected', () => { + mount('dark') + expect(screen.getByText('Appearance')).toBeDefined() + expect(pressed(/Dark/)).toBe('true') + expect(pressed(/Light/)).toBe('false') + expect(pressed(/System/)).toBe('false') + }) + + it('click drives setTheme; selection follows the store mirror, not the click echo', () => { + const b = mount('dark') + fireEvent.click(screen.getByRole('button', { name: /Light/ })) + expect(b.setTheme).toHaveBeenCalledWith('light') + // No store write yet: selection is unchanged. + expect(pressed(/Dark/)).toBe('true') + act(() => { b.store.actions.sync('light', 1) }) + expect(pressed(/Light/)).toBe('true') + expect(pressed(/Dark/)).toBe('false') + }) +}) diff --git a/packages/client/ui-theme/tests/settings-store.spec.ts b/packages/client/ui-theme/tests/settings-store.spec.ts new file mode 100644 index 0000000000..540d0f5b3b --- /dev/null +++ b/packages/client/ui-theme/tests/settings-store.spec.ts @@ -0,0 +1,28 @@ +/** Appearance row store: snapshot-mirror action and the revision guard. */ +import { describe, expect, it } from 'vitest' +import { createAppearanceRowStore } from '../src/client/settings-store.ts' + +describe('createAppearanceRowStore', () => { + it('init shape: system preference with revision at -1', () => { + const store = createAppearanceRowStore().create() + expect(store.getSnapshot()).toEqual({ preference: 'system', revision: -1 }) + }) + + it('sync mirrors the preference and advances the revision', () => { + const store = createAppearanceRowStore().create() + store.actions.sync('dark', 0) + expect(store.getSnapshot()).toEqual({ preference: 'dark', revision: 0 }) + store.actions.sync('light', 2) + expect(store.getSnapshot().preference).toBe('light') + expect(store.getSnapshot().revision).toBe(2) + }) + + it('revision guard drops stale and duplicate writes', () => { + const store = createAppearanceRowStore().create() + store.actions.sync('dark', 3) + store.actions.sync('system', 2) + store.actions.sync('system', 3) + expect(store.getSnapshot().preference).toBe('dark') + expect(store.getSnapshot().revision).toBe(3) + }) +})