| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546 |
- import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
- import { defaultPreferences } from '../src/config';
- import { isDarkTheme } from '../src/update-css-variables';
- describe('preferences', () => {
- let PreferenceManager: typeof import('../src/preferences').PreferenceManager;
- let preferenceManager: InstanceType<
- typeof import('../src/preferences').PreferenceManager
- >;
- // 模拟 window.matchMedia 方法
- vi.stubGlobal(
- 'matchMedia',
- vi.fn().mockImplementation((query) => ({
- addEventListener: vi.fn(),
- addListener: vi.fn(), // Deprecated
- dispatchEvent: vi.fn(),
- matches: query === '(prefers-color-scheme: dark)',
- media: query,
- onchange: null,
- removeEventListener: vi.fn(),
- removeListener: vi.fn(), // Deprecated
- })),
- );
- vi.stubGlobal('localStorage', {
- clear: vi.fn(),
- getItem: vi.fn(() => null),
- key: vi.fn(() => null),
- length: 0,
- removeItem: vi.fn(),
- setItem: vi.fn(),
- });
- vi.stubGlobal('sessionStorage', {
- clear: vi.fn(),
- getItem: vi.fn(() => null),
- key: vi.fn(() => null),
- length: 0,
- removeItem: vi.fn(),
- setItem: vi.fn(),
- });
- beforeAll(async () => {
- ({ PreferenceManager } = await import('../src/preferences'));
- });
- beforeEach(() => {
- vi.mocked(localStorage.getItem).mockImplementation(() => null);
- vi.mocked(localStorage.removeItem).mockReset();
- vi.mocked(localStorage.setItem).mockReset();
- vi.mocked(sessionStorage.getItem).mockImplementation(() => null);
- vi.mocked(sessionStorage.removeItem).mockReset();
- vi.mocked(sessionStorage.setItem).mockReset();
- preferenceManager = new PreferenceManager();
- });
- it('loads default preferences if no saved preferences found', () => {
- const preferences = preferenceManager.getPreferences();
- expect(preferences).toEqual(defaultPreferences);
- });
- it('initializes preferences with overrides', async () => {
- const overrides: any = {
- app: {
- locale: 'en-US',
- },
- };
- await preferenceManager.initPreferences({
- namespace: 'testNamespace',
- overrides,
- });
- // 等待防抖动操作完成
- // await new Promise((resolve) => setTimeout(resolve, 300)); // 等待100毫秒
- const expected = {
- ...defaultPreferences,
- app: {
- ...defaultPreferences.app,
- ...overrides.app,
- },
- };
- expect(preferenceManager.getPreferences()).toEqual(expected);
- });
- it('updates theme mode correctly', () => {
- preferenceManager.updatePreferences({
- theme: {
- mode: 'light',
- },
- });
- expect(preferenceManager.getPreferences().theme.mode).toBe('light');
- });
- it('updates color modes correctly', () => {
- preferenceManager.updatePreferences({
- app: { colorGrayMode: true, colorWeakMode: true },
- });
- expect(preferenceManager.getPreferences().app.colorGrayMode).toBe(true);
- expect(preferenceManager.getPreferences().app.colorWeakMode).toBe(true);
- });
- it('resets preferences to default', () => {
- // 先更新一些偏好设置
- preferenceManager.updatePreferences({
- theme: {
- mode: 'light',
- },
- });
- // 然后重置偏好设置
- preferenceManager.resetPreferences();
- expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);
- });
- it('updates isMobile correctly', () => {
- // 模拟移动端状态
- vi.stubGlobal(
- 'matchMedia',
- vi.fn().mockImplementation((query) => ({
- addEventListener: vi.fn(),
- addListener: vi.fn(),
- dispatchEvent: vi.fn(),
- matches: query === '(max-width: 768px)',
- media: query,
- onchange: null,
- removeEventListener: vi.fn(),
- removeListener: vi.fn(),
- })),
- );
- preferenceManager.updatePreferences({
- app: { isMobile: true },
- });
- expect(preferenceManager.getPreferences().app.isMobile).toBe(true);
- });
- it('updates the locale preference correctly', () => {
- preferenceManager.updatePreferences({
- app: { locale: 'en-US' },
- });
- expect(preferenceManager.getPreferences().app.locale).toBe('en-US');
- });
- it('updates the sidebar width correctly', () => {
- preferenceManager.updatePreferences({
- sidebar: { width: 200 },
- });
- expect(preferenceManager.getPreferences().sidebar.width).toBe(200);
- });
- it('updates the sidebar collapse state correctly', () => {
- preferenceManager.updatePreferences({
- sidebar: { collapsed: true },
- });
- expect(preferenceManager.getPreferences().sidebar.collapsed).toBe(true);
- });
- it('updates the navigation style type correctly', () => {
- preferenceManager.updatePreferences({
- navigation: { styleType: 'flat' },
- } as any);
- expect(preferenceManager.getPreferences().navigation.styleType).toBe(
- 'flat',
- );
- });
- it('resets preferences to default correctly', () => {
- // 先更新一些偏好设置
- preferenceManager.updatePreferences({
- app: { locale: 'en-US' },
- sidebar: { collapsed: true, width: 200 },
- theme: {
- mode: 'light',
- },
- });
- // 然后重置偏好设置
- preferenceManager.resetPreferences();
- expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);
- });
- it('does not update undefined preferences', () => {
- const originalPreferences = preferenceManager.getPreferences();
- preferenceManager.updatePreferences({
- app: { nonexistentField: 'value' },
- } as any);
- expect(preferenceManager.getPreferences()).toEqual(originalPreferences);
- });
- it('reverts to default when a preference field is deleted', () => {
- preferenceManager.updatePreferences({
- app: { locale: 'en-US' },
- });
- preferenceManager.updatePreferences({
- app: { locale: undefined },
- });
- expect(preferenceManager.getPreferences().app.locale).toBe('en-US');
- });
- it('ignores updates with invalid preference value types', () => {
- const originalPreferences = preferenceManager.getPreferences();
- preferenceManager.updatePreferences({
- app: { isMobile: 'true' as unknown as boolean }, // 错误类型
- });
- expect(preferenceManager.getPreferences()).toEqual(originalPreferences);
- });
- it('merges nested preference objects correctly', () => {
- preferenceManager.updatePreferences({
- app: { name: 'New App Name' },
- });
- const expected = {
- ...defaultPreferences,
- app: {
- ...defaultPreferences.app,
- name: 'New App Name',
- },
- };
- expect(preferenceManager.getPreferences()).toEqual(expected);
- });
- it('applies updates immediately after initialization', async () => {
- const overrides: any = {
- app: {
- locale: 'en-US',
- },
- };
- await preferenceManager.initPreferences({
- namespace: 'apply-updates',
- overrides,
- });
- preferenceManager.updatePreferences({
- theme: { mode: 'light' },
- });
- expect(preferenceManager.getPreferences().theme.mode).toBe('light');
- });
- it('initializes custom preferences extension with default values', async () => {
- const extension = {
- fields: [
- {
- component: 'switch',
- defaultValue: true,
- key: 'enableWorkbench',
- label: '启用工作台',
- },
- {
- component: 'select',
- defaultValue: 'single',
- key: 'tenantMode',
- label: '租户模式',
- options: [
- { label: '单租户', value: 'single' },
- { label: '多租户', value: 'multi' },
- ],
- },
- ],
- tabLabel: '扩展',
- title: '业务偏好',
- } as const;
- await preferenceManager.initPreferences({
- extension,
- namespace: 'custom-defaults',
- });
- expect(preferenceManager.getPreferencesExtension()).toEqual(extension);
- expect(preferenceManager.getCustomPreferences()).toEqual({
- enableWorkbench: true,
- tenantMode: 'single',
- });
- });
- it('does not expose mutable custom preference baselines or extension schema', async () => {
- const extension = {
- fields: [
- {
- component: 'number',
- componentProps: {
- max: 10,
- min: 2,
- step: 2,
- },
- defaultValue: 4,
- key: 'pageSize',
- label: '分页大小',
- },
- ],
- tabLabel: '扩展',
- title: '业务偏好',
- } as const;
- await preferenceManager.initPreferences({
- extension,
- namespace: 'custom-readonly',
- });
- const initialCustomPreferences =
- preferenceManager.getInitialCustomPreferences<{
- pageSize: number;
- }>() as { pageSize: number };
- const preferencesExtension = preferenceManager.getPreferencesExtension<{
- pageSize: number;
- }>() as {
- fields: Array<{ componentProps?: { max?: number }; label: string }>;
- };
- const [firstField] = preferencesExtension.fields;
- initialCustomPreferences.pageSize = 8;
- expect(firstField).toBeDefined();
- expect(firstField?.componentProps).toBeDefined();
- if (!firstField || !firstField.componentProps) {
- return;
- }
- firstField.label = '已修改';
- firstField.componentProps.max = 20;
- expect(preferenceManager.getInitialCustomPreferences()).toEqual({
- pageSize: 4,
- });
- expect(preferenceManager.getPreferencesExtension()).toEqual(extension);
- });
- it('updates and resets custom preferences correctly', async () => {
- await preferenceManager.initPreferences({
- extension: {
- fields: [
- {
- component: 'number',
- defaultValue: 20,
- key: 'pageSize',
- label: '分页大小',
- },
- {
- component: 'input',
- defaultValue: '日报',
- key: 'reportTitle',
- label: '报表标题',
- },
- ],
- tabLabel: '扩展',
- },
- namespace: 'custom-reset',
- });
- preferenceManager.updateCustomPreferences({
- pageSize: 50,
- reportTitle: '月报',
- });
- expect(preferenceManager.getCustomPreferences()).toEqual({
- pageSize: 50,
- reportTitle: '月报',
- });
- preferenceManager.resetPreferences();
- expect(preferenceManager.getCustomPreferences()).toEqual({
- pageSize: 20,
- reportTitle: '日报',
- });
- });
- it('ignores invalid custom preferences updates', async () => {
- await preferenceManager.initPreferences({
- extension: {
- fields: [
- {
- component: 'switch',
- defaultValue: true,
- key: 'enableWorkbench',
- label: '启用工作台',
- },
- {
- component: 'select',
- defaultValue: 'single',
- key: 'tenantMode',
- label: '租户模式',
- options: [
- { label: '单租户', value: 'single' },
- { label: '多租户', value: 'multi' },
- ],
- },
- ],
- tabLabel: '扩展',
- },
- namespace: 'custom-invalid',
- });
- const originalCustomPreferences = preferenceManager.getCustomPreferences();
- preferenceManager.updateCustomPreferences({
- enableWorkbench: 'true' as unknown as boolean,
- tenantMode: 'unknown',
- unknownField: 'value',
- } as any);
- expect(preferenceManager.getCustomPreferences()).toEqual(
- originalCustomPreferences,
- );
- });
- it('enforces custom number field min max and step constraints', async () => {
- await preferenceManager.initPreferences({
- extension: {
- fields: [
- {
- component: 'number',
- componentProps: {
- max: 10,
- min: 2,
- step: 2,
- },
- defaultValue: 4,
- key: 'pageSize',
- label: '分页大小',
- },
- ],
- tabLabel: '扩展',
- },
- namespace: 'custom-number-constraints',
- });
- preferenceManager.updateCustomPreferences({
- pageSize: 8,
- });
- expect(preferenceManager.getCustomPreferences()).toEqual({
- pageSize: 8,
- });
- preferenceManager.updateCustomPreferences({
- pageSize: 1,
- });
- expect(preferenceManager.getCustomPreferences()).toEqual({
- pageSize: 8,
- });
- preferenceManager.updateCustomPreferences({
- pageSize: 12,
- });
- expect(preferenceManager.getCustomPreferences()).toEqual({
- pageSize: 8,
- });
- preferenceManager.updateCustomPreferences({
- pageSize: 5,
- });
- expect(preferenceManager.getCustomPreferences()).toEqual({
- pageSize: 8,
- });
- });
- it('filters cached custom number values that violate field constraints', async () => {
- vi.mocked(localStorage.getItem).mockImplementation((key) => {
- if (key.endsWith('cache-preferences-custom')) {
- return JSON.stringify({
- value: {
- pageSize: 5,
- },
- });
- }
- return null;
- });
- await preferenceManager.initPreferences({
- extension: {
- fields: [
- {
- component: 'number',
- componentProps: {
- max: 10,
- min: 2,
- step: 2,
- },
- defaultValue: 4,
- key: 'pageSize',
- label: '分页大小',
- },
- ],
- tabLabel: '扩展',
- },
- namespace: 'custom-number-cache',
- });
- expect(preferenceManager.getCustomPreferences()).toEqual({
- pageSize: 4,
- });
- });
- });
- describe('isDarkTheme', () => {
- it('should return true for dark theme', () => {
- expect(isDarkTheme('dark')).toBe(true);
- });
- it('should return false for light theme', () => {
- expect(isDarkTheme('light')).toBe(false);
- });
- it('should return system preference for auto theme', () => {
- vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
- addEventListener: vi.fn(),
- addListener: vi.fn(), // Deprecated
- dispatchEvent: vi.fn(),
- matches: query === '(prefers-color-scheme: dark)',
- media: query,
- onchange: null,
- removeEventListener: vi.fn(),
- removeListener: vi.fn(), // Deprecated
- }));
- expect(isDarkTheme('auto')).toBe(true);
- expect(window.matchMedia).toHaveBeenCalledWith(
- '(prefers-color-scheme: dark)',
- );
- });
- });
|