diff --git a/web/src/app/settings.test.ts b/web/src/app/settings.test.ts index 9d3011b..53b067c 100644 --- a/web/src/app/settings.test.ts +++ b/web/src/app/settings.test.ts @@ -8,7 +8,7 @@ import ShortcutSettings from './ShortcutSettings.vue' import { DEFAULTS, loadSettings, settings, settingsSave, SettingsAppearanceError, updateSettings } from './settings' import { searchSettings } from './settingsCatalog' import { ws } from './workspace' -import { applyFontCatalog } from './fonts' +import { applyFontCatalog, customFonts } from './fonts' let app: App | undefined let host: HTMLElement | undefined @@ -194,6 +194,50 @@ describe('设置查找与重置', () => { }) }) +describe('设置同步顺序', () => { + it.each(['settings', 'fonts'] as const)('旧%s响应晚到时,不恢复已删除字体或覆盖新设置与缓存', async (delayed) => { + const font = { id: 'custom-' + 'c'.repeat(64), name: '已删除字体', format: 'woff2' as const } + const oldSettings = { uiFont: font.id, textFont: font.id, codeFont: font.id, fontSize: 18 } + let release!: () => void + const pending = new Promise(resolve => { release = resolve }) + vi.mocked(api.settings).mockImplementationOnce(async () => { + if (delayed === 'settings') await pending + return oldSettings + }) + vi.mocked(api.fonts).mockImplementationOnce(async () => { + if (delayed === 'fonts') await pending + return [font] + }) + const oldLoad = loadSettings() + vi.mocked(api.settings).mockResolvedValue({ uiFont: 'maple', textFont: 'maple', codeFont: 'maple', fontSize: 24 }) + await loadSettings() + const cache = localStorage.getItem('rawmark.settings') + release() + await oldLoad + expect(settings).toMatchObject({ uiFont: 'maple', textFont: 'maple', codeFont: 'maple', fontSize: 24 }) + expect(customFonts.value).toEqual([]) + expect(document.querySelector('#rm-custom-fonts')!.textContent).not.toContain(font.id) + expect(document.documentElement.style.getPropertyValue('--rm-font-text')).not.toContain(font.id) + expect(document.documentElement.style.getPropertyValue('--rm-font-size')).toBe('24px') + expect(localStorage.getItem('rawmark.settings')).toBe(cache) + }) + + it('最新同步失败仍报告原错误,迟到旧响应不会覆盖当前状态,后续可重新同步', async () => { + let release!: (value: Record) => void + vi.mocked(api.settings).mockImplementationOnce(() => new Promise(resolve => { release = resolve })) + const oldLoad = loadSettings() + const cause = new ApiError(503, '字体列表暂时不可用') + vi.mocked(api.fonts).mockRejectedValueOnce(cause) + await expect(loadSettings()).rejects.toBe(cause) + release({ fontSize: 18 }) + await oldLoad + expect(settings.fontSize).toBe(DEFAULTS.fontSize) + vi.mocked(api.settings).mockResolvedValue({ fontSize: 24 }) + await loadSettings() + expect(settings.fontSize).toBe(24) + }) +}) + describe('保存反馈', () => { it('所有排队请求完成之前保留保存中,不提前发送后续请求', async () => { let finish!: (value: Record) => void diff --git a/web/src/app/settings.ts b/web/src/app/settings.ts index 0c6890e..175c0dc 100644 --- a/web/src/app/settings.ts +++ b/web/src/app/settings.ts @@ -94,8 +94,12 @@ applied({ ...settings }) // 缓存只提前应用明暗模式;主题和片段等服务器确认后再应用。 applyAppearance({ ...settings, theme: 'default', cssSnippets: [] }, '') +let settingsRequest = 0 export async function loadSettings() { + const request = ++settingsRequest const [raw, fonts] = await Promise.all([api.settings(), api.fonts()]) + // 目录与设置作为一批应用,迟到旧响应不能恢复已删除字体或覆盖新设置。 + if (request !== settingsRequest) return applyFontCatalog(parseFontCatalog(fonts)) applied(sanitize(raw)) await applySettingsAppearance()