1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-12 16:40:40 +08:00
Files
uni-halo/src/utils/preference.test.ts
T
小莫唐尼 beaf2c39ca feat: 实现全站布局偏好系统,支持多页面独立配置
本次提交完成了完整的偏好设置体系重构:
1. 新增preferences.md文档说明布局偏好设计与映射规则
2. 重构布局配置结构,按页面分组管理列表布局与卡片样式
3. 实现后端preferences字段到本地配置的自动映射转换
4. 完成首页、文章列表、归档页的布局动态切换逻辑
5. 重构投票组件样式绑定,改用unocss类名实现样式分离
6. 新增偏好设置页面,支持按页面配置布局参数
7. 兼容旧版布局参数的自动归一化转换
8. 补充完整的单元测试用例覆盖新旧数据迁移场景
2026-09-09 14:48:55 +08:00

202 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { DefaultAppSettings } from '@/config/appSettings'
import type { IAppSettings } from '@/config/appSettings'
import {
LOCAL_PREFS_KEY,
clearLocalPrefs,
collectSiteDefaults,
isLocalOverride,
mergeWithDefaults,
migrateLegacyLocalPrefs,
readLocalPrefs,
updateLocalPrefs,
} from './preference'
/** 内存版 uni storage(与 utils/storage 的 {data,time,expire} 包装配合) */
const mem = new Map<string, string>()
function setupUniStorageMock() {
vi.mocked(uni.getStorageSync).mockImplementation((key: string) => mem.get(key) ?? null)
vi.mocked(uni.setStorageSync).mockImplementation((key: string, val: unknown) => {
mem.set(key, val as string)
})
vi.mocked(uni.removeStorageSync).mockImplementation((key: string) => {
mem.delete(key)
})
}
describe('preference 基础读写', () => {
beforeEach(() => {
mem.clear()
setupUniStorageMock()
})
it('readLocalPrefs:无数据时返回空对象', () => {
expect(readLocalPrefs()).toEqual({})
})
it('updateLocalPrefs:嵌套字段增量合并', () => {
updateLocalPrefs({ layout: { home: { listLayout: 'double' } } })
updateLocalPrefs({ layout: { home: { cardType: 'image_bottom' } } })
expect(readLocalPrefs()).toEqual({
layout: { home: { listLayout: 'double', cardType: 'image_bottom' } },
})
})
it('updateLocalPrefsnull 删除该键(回退跟随站点默认)', () => {
updateLocalPrefs({ layout: { home: { listLayout: 'double', cardType: 'image_bottom' } } })
updateLocalPrefs({ layout: { home: null } })
expect(readLocalPrefs()).toEqual({ layout: {} })
})
it('updateLocalPrefs(null):整体清空差异', () => {
updateLocalPrefs({ layout: { home: { listLayout: 'double' } } })
updateLocalPrefs(null)
expect(readLocalPrefs()).toEqual({})
expect(mem.has(LOCAL_PREFS_KEY)).toBe(false)
})
it('clearLocalPrefs:删除存储键', () => {
updateLocalPrefs({ layout: { home: { listLayout: 'double' } } })
clearLocalPrefs()
expect(readLocalPrefs()).toEqual({})
})
it('isLocalOverride:按路径判断是否被本地覆盖', () => {
updateLocalPrefs({ gallery: { useWaterfull: false } })
expect(isLocalOverride(readLocalPrefs(), ['gallery', 'useWaterfull'])).toBe(true)
expect(isLocalOverride(readLocalPrefs(), ['banner', 'useDot'])).toBe(false)
})
})
describe('mergeWithDefaults / collectSiteDefaults', () => {
it('默认值兜底:无站点默认无本地差异时等于内置默认', () => {
expect(mergeWithDefaults()).toEqual(DefaultAppSettings)
})
it('本地优先于站点默认,站点默认优先于内置默认', () => {
const merged = mergeWithDefaults(
{ layout: { home: { listLayout: 'single' } }, gallery: { useWaterfull: true } },
{ layout: { home: { listLayout: 'double' } } },
)
expect(merged.layout.home.listLayout).toBe('double')
expect(merged.gallery.useWaterfull).toBe(true)
expect(merged.isAvatarRadius).toBe(DefaultAppSettings.isAvatarRadius)
})
it('collectSiteDefaultsbanner 站点默认映射(showIndicator→useDot)', () => {
const site = collectSiteDefaults({
pageConfig: {
homeConfig: {
bannerConfig: { showIndicator: false, dotPosition: 'bottom' },
},
},
})
expect(site.banner).toEqual({ useDot: false, dotPosition: 'bottom' })
})
it('collectSiteDefaultspreferences(L0)映射到 layout 页面分组/isAvatarRadius', () => {
const site = collectSiteDefaults({
preferences: {
homeListLayout: 'h_row_col2',
homeCardType: 'image_bottom',
articlesListLayout: 'single',
articleCardType: 'image_left',
archivesCardType: 'image_top',
avatarRadius: true,
},
})
expect(site.layout).toEqual({
home: { listLayout: 'double', cardType: 'image_bottom' },
articles: { listLayout: 'single', cardType: 'image_left' },
archives: { cardType: 'image_top' },
})
expect(site.isAvatarRadius).toBe(true)
})
it('preferences L0 参与合并,本地未覆盖时跟随站点默认', () => {
const site = collectSiteDefaults({
preferences: {
homeListLayout: 'h_row_col2',
articleCardType: 'image_bottom',
avatarRadius: true,
},
})
const merged = mergeWithDefaults(site, {})
expect(merged.layout.home.listLayout).toBe('double')
expect(merged.layout.articles.cardType).toBe('image_bottom')
expect(merged.isAvatarRadius).toBe(true)
})
it('preferences L0 可被本地差异覆盖,重置后回退站点默认', () => {
const site = collectSiteDefaults({
preferences: {
homeListLayout: 'h_row_col2',
articleCardType: 'image_bottom',
avatarRadius: true,
},
})
const merged = mergeWithDefaults(site, { layout: { home: { listLayout: 'single' } }, isAvatarRadius: false })
expect(merged.layout.home.listLayout).toBe('single')
expect(merged.layout.articles.cardType).toBe('image_bottom')
expect(merged.isAvatarRadius).toBe(false)
})
it('站点 banner 默认参与合并,本地未覆盖时跟随站点默认', () => {
const site = collectSiteDefaults({
pageConfig: {
homeConfig: {
bannerConfig: { showIndicator: false, dotPosition: 'bottom' },
},
},
})
const merged = mergeWithDefaults(site, {})
expect(merged.banner.useDot).toBe(false)
expect(merged.banner.dotPosition).toBe('bottom')
expect(merged.layout.home.listLayout).toBe(DefaultAppSettings.layout.home.listLayout)
})
it('未知枚举值不回退抛错(跟随默认)', () => {
const merged = mergeWithDefaults({}, { layout: { home: { listLayout: 'not-exist' } } })
expect(merged.layout.home.listLayout).toBe('not-exist')
})
})
describe('migrateLegacyLocalPrefs', () => {
beforeEach(() => {
mem.clear()
setupUniStorageMock()
})
it('旧 persist 存在时仅迁移被改过的叶子字段', () => {
const legacySettings: IAppSettings = JSON.parse(JSON.stringify(DefaultAppSettings))
// 旧结构 layout.home 为 string(列表布局),类型上绕过新结构约束
;(legacySettings.layout as unknown as Record<string, unknown>).home = 'h_row_col2'
legacySettings.gallery.useWaterfull = false
mem.set('setting', JSON.stringify({ settings: legacySettings }))
expect(migrateLegacyLocalPrefs()).toBe(true)
expect(readLocalPrefs()).toEqual({
layout: { home: 'h_row_col2' },
gallery: { useWaterfull: false },
})
})
it('已存在新差异键时不再重复迁移', () => {
updateLocalPrefs({ layout: { home: { listLayout: 'single' } } })
const legacySettings = JSON.parse(JSON.stringify(DefaultAppSettings)) as IAppSettings
;(legacySettings.layout as unknown as Record<string, unknown>).home = 'h_row_col2'
mem.set('setting', JSON.stringify({ settings: legacySettings }))
expect(migrateLegacyLocalPrefs()).toBe(false)
expect(readLocalPrefs()).toEqual({ layout: { home: { listLayout: 'single' } } })
})
it('无旧键或格式异常时返回 false 且不写新键', () => {
expect(migrateLegacyLocalPrefs()).toBe(false)
mem.set('setting', 'not-json{')
expect(migrateLegacyLocalPrefs()).toBe(false)
expect(mem.has(LOCAL_PREFS_KEY)).toBe(false)
})
})