Files
MailUI4Agents/client/electron/test/components/BackgroundPicker.test.tsx
JianFeeeee 5804ba4f63 fix(webui): 自定义背景完全不可用 —— 「图片」档进不去
现象:点「图片」后背景反而被关掉,上传控件永远不出现 ⇒ 自定义图片在 UI 上
完全不可达(用户看到的正是"自定义背景不正常")。

根因:`normalizeBackground` 把「kind=image 但还没有图片数据」折叠成 `none`
(这条判据本身是对的 —— 读盘时那确实是脏数据),但 store 的 `commit()` 每次
patch 都要过一遍它,于是 `setKind('image')` 这一瞬间就被折叠回去;而上传控件
只在 `kind === 'image'` 下渲染 ⇒ 鸡生蛋问题,用户永远走不到选文件那一步。

修法:给归一化加 `keepEmptyImage`。
  - 读盘(`readStored`)保持严格:空图片状态是脏数据,退回 none。
  - 交互(`commit`)保留瞬态:允许"已选图片档、还没挑文件"这个中间状态存在。
静止态的不变量没有放松,放松的只是正在选图的那一瞬间;`applyBackground` 对
空图片本来就不铺开(不会出现 `url("")`)。

判据(都验过"修复前会红"):
  · 新增 `test/components/BackgroundPicker.test.tsx` —— 点「图片」后选文件控件
    必须出现、选完图背景必须亮、失败必须说原因、选「无」必须能关掉。
    **扰动验证**:把修复撤掉 → 组件 3 红 + store 1 红;恢复 → 22 全绿。
  · `test/stores/background.test.ts` 补 2 条:交互进入图片档要留住 /
    瞬态落盘后重读必须退回 none。

线上验证(真浏览器,部署后):线上 bundle 换成 index-CSFGa8wa.js 后 ——
点「图片」→ `kind=image` 保持 → 上传 16KB 小图与 9MB 大图都成功
(大图压缩到 1790KB)→ 刷新后仍在 → 全程无页面错误。

顺带:应用内**从未出现**过品牌图标。`BrandMarkIcon` 只用在登录页与首启页
(都是登录前界面),登录后的日常界面里一处都没有。侧栏顶端加上品牌标记
(点它回收件箱)。favicon 那条链本来就是好的(3 个 link 都 200、类型正确、
图标内容正确),已在验证中确认。
2026-09-13 08:55:55 +08:00

80 lines
3.6 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.

/**
* 背景选择器的**界面可达性**判据。
*
* 为什么需要这一层:`stores/background.test.ts` 只钉了 store 的状态机,
* 而线上真实缺陷恰好落在"状态 → 控件"这一跳上 ——
* 点「图片」时 kind 被折叠回 `none`,于是上传控件(只在 `kind === 'image'`
* 下渲染)**从不出现**store 的单测全绿,用户却完全无法自定义背景。
*
* 所以这里测的是"用户走得到走不到":点「图片」→ 选文件按钮必须出现 →
* 选完图背景必须真的亮起来。
*/
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BackgroundPicker from '../../src/components/BackgroundPicker';
import { STORAGE_KEY, useBackgroundStore } from '../../src/stores/backgroundStore';
import * as store from '../../src/stores/backgroundStore';
describe('BackgroundPicker', () => {
beforeEach(() => {
localStorage.clear();
document.documentElement.removeAttribute('data-bg');
document.documentElement.className = '';
document.documentElement.removeAttribute('style');
useBackgroundStore.setState({ ...store.DEFAULT_BACKGROUND });
vi.restoreAllMocks();
});
it('★ 点「图片」后必须出现选文件控件(否则自定义背景根本走不到)', async () => {
render(<BackgroundPicker />);
// 初始是「无」:没有文件输入
expect(document.querySelector('input[type=file]')).toBeNull();
await userEvent.click(screen.getByRole('button', { name: '图片' }));
expect(useBackgroundStore.getState().kind).toBe('image');
expect(document.querySelector('input[type=file]')).not.toBeNull();
expect(screen.getByRole('button', { name: /选择图片|更换图片/ })).toBeInTheDocument();
});
it('★ 选完图片后背景真的亮起来,并落盘', async () => {
vi.spyOn(store, 'prepareImage').mockResolvedValue({
ok: true,
dataUrl: 'data:image/jpeg;base64,ZZZZ'
});
render(<BackgroundPicker />);
await userEvent.click(screen.getByRole('button', { name: '图片' }));
const input = document.querySelector('input[type=file]') as HTMLInputElement;
const file = new File(['x'], 'bg.jpg', { type: 'image/jpeg' });
await userEvent.upload(input, file);
await waitFor(() => expect(document.documentElement.dataset.bg).toBe('on'));
expect(document.documentElement.style.getPropertyValue('--bg-image')).toContain('ZZZZ');
expect(JSON.parse(localStorage.getItem(STORAGE_KEY)!).kind).toBe('image');
});
it('图片准备失败时说出原因,并且不假装成功', async () => {
vi.spyOn(store, 'prepareImage').mockResolvedValue({ ok: false, reason: '图片过大(超过 20MB请先裁剪' });
render(<BackgroundPicker />);
await userEvent.click(screen.getByRole('button', { name: '图片' }));
const input = document.querySelector('input[type=file]') as HTMLInputElement;
await userEvent.upload(input, new File(['x'], 'bg.jpg', { type: 'image/jpeg' }));
expect(await screen.findByRole('alert')).toHaveTextContent('图片过大');
// 没换成就不该亮背景
expect(document.documentElement.dataset.bg).toBe('off');
});
it('「无」把背景关掉(对照:图片档不是永久卡住)', async () => {
useBackgroundStore.getState().setImage('data:image/png;base64,YYYY');
render(<BackgroundPicker />);
await userEvent.click(screen.getByRole('button', { name: '无' }));
expect(useBackgroundStore.getState().kind).toBe('none');
expect(document.documentElement.dataset.bg).toBe('off');
});
});