import { expect, it, vi } from 'vitest'; const state = vi.hoisted(() => ({ slots: [] as any[], cursor: 0, resourceCursor: 0, resources: [] as any[], })); vi.mock('react', async original => ({ ...await original(), useState(value: unknown) { const slots = state.slots, i = state.cursor++; if (!(i in slots)) slots[i] = value; return [slots[i], (next: unknown) => {slots[i] = typeof next === 'function' ? next(slots[i]) : next;}]; }, })); vi.mock('./ui', async original => ({ ...await original(), useResource: () => state.resources[state.resourceCursor++], useAction: () => ({busy: false, feedback: null}), })); vi.mock('./components/ConfirmDialog', () => ({useConfirm: () => ({element: null})})); vi.mock('./ProfileEditor', () => ({ProfileEditor: () => null})); import { Instances } from './Instances'; import { ProfileEditor } from './ProfileEditor'; import { BackupPanel } from './BackupPanel'; function nodes(root: any, predicate: (node: any) => boolean): any[] { if (Array.isArray(root)) return root.flatMap(child => nodes(child, predicate)); if (!root || typeof root !== 'object') return []; return [...(predicate(root) ? [root] : []), ...nodes(root.props?.children, predicate)]; } it('preserves the unsaved Profile draft on backup refresh and reloads it only after restore', async () => { const instance = {id: 'one', name: 'Test instance', status: 'stopped', created_at: '', version: 'test'}; const instances = {data: [instance], loading: false, refresh: vi.fn(async () => {})}; const usage = {data: {used_bytes: 1, quota_bytes: 100}, loading: false, refresh: vi.fn(async () => {})}; const profile = { data: {content: 'name: original'}, loading: false, refresh: vi.fn(async () => {profile.data = {content: 'name: restored'};}), }; const parentSlots: any[] = [], detailSlots: any[] = []; function renderParent() { state.slots = parentSlots; state.cursor = 0; state.resourceCursor = 0; state.resources = [instances, usage]; return Instances(); } function renderDetail() { const detail = nodes(renderParent(), n => typeof n.type === 'function' && n.props.instance?.id === instance.id)[0]; expect(detail).toBeDefined(); state.slots = detailSlots; state.cursor = 0; state.resourceCursor = 0; state.resources = [{data: []}, profile, {data: undefined}, {data: undefined}]; return detail.type(detail.props); } const editor = (tree: any) => nodes(tree, n => n.type === ProfileEditor && n.props.onChange)[0]; const backup = (tree: any) => nodes(tree, n => n.type === BackupPanel)[0]; const save = (tree: any) => nodes(tree, n => n.type === 'button' && n.props.children === '保存并启动')[0]; nodes(renderParent(), n => n.type === 'button' && n.props.children === '管理')[0].props.onClick(); let tree = renderDetail(); expect(editor(tree).props.value).toBe('name: original'); expect(save(tree).props.disabled).toBe(true); editor(tree).props.onChange('name: unsaved draft'); tree = renderDetail(); expect(editor(tree).props.value).toBe('name: unsaved draft'); expect(save(tree).props.disabled).toBe(false); await backup(tree).props.refresh(); expect(instances.refresh).toHaveBeenCalledOnce(); expect(usage.refresh).toHaveBeenCalledOnce(); expect(profile.refresh).not.toHaveBeenCalled(); tree = renderDetail(); expect(editor(tree).props.value).toBe('name: unsaved draft'); expect(save(tree).props.disabled).toBe(false); await backup(tree).props.onRestored(); expect(profile.refresh).toHaveBeenCalledOnce(); expect(instances.refresh).toHaveBeenCalledTimes(2); expect(usage.refresh).toHaveBeenCalledTimes(2); tree = renderDetail(); expect(editor(tree).props.value).toBe('name: restored'); expect(save(tree).props.disabled).toBe(true); });