Add snapshot usage and protected cleanup, preserve unsaved profile edits, avoid holding SQL connections during snapshot deletion, and fix download filenames. Include regression tests and API documentation.
82 lines
3.7 KiB
TypeScript
82 lines
3.7 KiB
TypeScript
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<typeof import('react')>(),
|
|
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<typeof import('./ui')>(),
|
|
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);
|
|
});
|