现象:点「图片」后背景反而被关掉,上传控件永远不出现 ⇒ 自定义图片在 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、类型正确、
图标内容正确),已在验证中确认。
80 lines
3.6 KiB
TypeScript
80 lines
3.6 KiB
TypeScript
/**
|
||
* 背景选择器的**界面可达性**判据。
|
||
*
|
||
* 为什么需要这一层:`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');
|
||
});
|
||
});
|