mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-10 21:11:08 +08:00
69 lines
5.0 KiB
TypeScript
69 lines
5.0 KiB
TypeScript
import { render } from '../src/shared/ui/render-root.ts';
|
|
// @vitest-environment jsdom
|
|
import {afterEach,expect,it,vi} from 'vitest';
|
|
import {act} from 'react';
|
|
|
|
import {MusicParentEditor} from '../src/features/media/components/parent-editor.tsx';
|
|
import {ApiError} from '../src/api/errors.ts';
|
|
import type {Detail,MediaApi} from '../src/features/media/api/media-api.ts';
|
|
const root=document.createElement('div');document.body.append(root);
|
|
afterEach(()=>act(()=>render(null,root)));
|
|
const button=(name:string)=>[...root.querySelectorAll('button')].find(value=>value.textContent===name)!;
|
|
it('previews and cancels creation without writing, then creates with the expected parent',async()=>{
|
|
const item:Detail={id:'track',libraryId:'lib',kind:'track',parentId:'old',title:'曲目',metadata:{},overrides:{},editions:[],children:[]};
|
|
const request=vi.fn().mockResolvedValue({...item,parentId:'created'}),updated=vi.fn();
|
|
const api={request,items:vi.fn(async()=>({items:[],total:0}))} as unknown as MediaApi;
|
|
await act(async()=>render(<MusicParentEditor api={api} item={item} onUpdated={updated}/>,root));
|
|
await act(async()=>button('调整所属专辑').click());
|
|
const input=root.querySelector<HTMLInputElement>('input[aria-label="新建专辑名称"]')!;
|
|
await vi.waitFor(()=>expect(input.disabled).toBe(false));
|
|
await act(async()=>{input.value=' 新专辑 ';input.dispatchEvent(new Event('input',{bubbles:true}));});
|
|
await act(async()=>button('预览新建专辑').click());
|
|
expect(request).not.toHaveBeenCalled();expect(root.textContent).toContain('新专辑沿用原专辑的歌手');
|
|
await act(async()=>button('取消新建').click());expect(request).not.toHaveBeenCalled();
|
|
await act(async()=>button('预览新建专辑').click());
|
|
await act(async()=>button('确认新建并调整归属').click());
|
|
expect(request).toHaveBeenCalledTimes(1);
|
|
expect(request).toHaveBeenCalledWith('items/track/parent','POST',{title:'新专辑',expectedParentId:'old'},expect.any(AbortSignal));
|
|
expect(updated).toHaveBeenCalledWith(expect.objectContaining({parentId:'created'}));
|
|
});
|
|
it('locks creation after a stale-parent conflict instead of replaying it',async()=>{
|
|
const item:Detail={id:'album',libraryId:'lib',kind:'album',parentId:'old',title:'专辑',metadata:{},overrides:{},editions:[],children:[]};
|
|
const request=vi.fn().mockRejectedValue(new ApiError('conflict','归属变化','MEDIA_PARENT_CHANGED',409));
|
|
const api={request,items:vi.fn(async()=>({items:[],total:0}))} as unknown as MediaApi;
|
|
await act(async()=>render(<MusicParentEditor api={api} item={item} onUpdated={()=>{}}/>,root));
|
|
await act(async()=>button('调整所属歌手').click());
|
|
const input=root.querySelector<HTMLInputElement>('input[aria-label="新建歌手名称"]')!;
|
|
await vi.waitFor(()=>expect(input.disabled).toBe(false));
|
|
await act(async()=>{input.value='新歌手';input.dispatchEvent(new Event('input',{bubbles:true}));});
|
|
await act(async()=>button('预览新建歌手').click());
|
|
await act(async()=>button('确认新建并调整归属').click());
|
|
expect(button('预览新建歌手').disabled).toBe(true);
|
|
expect(button('确认新建并调整归属')).toBeUndefined();expect(request).toHaveBeenCalledTimes(1);
|
|
});
|
|
it('requires a fresh parent after conflict and retries reads without replaying the write',async()=>{
|
|
let item:Detail={id:'track',libraryId:'lib',kind:'track',parentId:'old',title:'曲目',metadata:{},overrides:{},editions:[],children:[]};
|
|
const target={...item,id:'target',kind:'album',title:'目标专辑',parentId:null};
|
|
const request=vi.fn().mockRejectedValueOnce(new ApiError('conflict','归属变化','MEDIA_PARENT_CHANGED',409)).mockResolvedValue({...item,parentId:'target'});
|
|
const detail=vi.fn().mockRejectedValueOnce(new Error('刷新失败')).mockResolvedValue({...item,parentId:'new-parent'});
|
|
const items=vi.fn(async()=>({items:[target],total:1}));
|
|
const api={request,detail,items} as unknown as MediaApi;
|
|
const draw=()=>render(<MusicParentEditor api={api} item={item} onUpdated={value=>{item=value;draw();}}/>,root);
|
|
await act(async()=>draw());
|
|
await act(async()=>button('调整所属专辑').click());
|
|
await vi.waitFor(()=>expect(button('选择')?.disabled).toBe(false));
|
|
await act(async()=>button('选择').click());
|
|
await act(async()=>button('确认调整归属').click());
|
|
await vi.waitFor(()=>expect(button('刷新当前归属')).toBeDefined());
|
|
expect(button('选择').disabled).toBe(true);
|
|
await act(async()=>button('刷新当前归属').click());
|
|
await vi.waitFor(()=>expect(root.textContent).toContain('刷新失败'));
|
|
expect(request).toHaveBeenCalledTimes(1);expect(button('选择').disabled).toBe(true);
|
|
await act(async()=>button('刷新当前归属').click());
|
|
await vi.waitFor(()=>expect(button('选择')?.disabled).toBe(false));
|
|
expect(request).toHaveBeenCalledTimes(1);
|
|
await act(async()=>button('选择').click());
|
|
await act(async()=>button('确认调整归属').click());
|
|
expect(request).toHaveBeenLastCalledWith('items/track/parent','PUT',{targetParentId:'target',expectedParentId:'new-parent'},expect.any(AbortSignal));
|
|
});
|