Add exact-image compatibility policy, experimental admission, paginated build history and shared reauthentication checks. Preserve uncertain Docker creation records, bound frontend resource waits and renew runtime sessions proactively. Lazy-load and split Monaco, add regression and opt-in live Docker fault tests, and record full race and real-browser acceptance results.
128 lines
8.3 KiB
TypeScript
128 lines
8.3 KiB
TypeScript
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
const state = vi.hoisted(() => ({slots: [] as any[], cursor: 0}));
|
|
vi.mock('react', async original => ({
|
|
...await original<typeof import('react')>(),
|
|
useState(value: unknown) {const i = state.cursor++; if (!(i in state.slots)) state.slots[i] = value; return [state.slots[i], (next: unknown) => {state.slots[i] = next;}];},
|
|
useRef(value: unknown) {const i = state.cursor++; return state.slots[i] ??= {current:value};},
|
|
useEffect() {},
|
|
}));
|
|
vi.mock('./ui', async original => ({
|
|
...await original<typeof import('./ui')>(),
|
|
useResource: (load: unknown) => ({data: load === api.compatibility ? policy : load === api.images ? [{id:'ok', version:'1.2.3', status:'available', selectable:true}, {id:'exp', version:'9.9.9', status:'experimental', selectable:false}] : [], loading:false, refresh:vi.fn(async () => {})}),
|
|
}));
|
|
import { api, type Compatibility } from './api';
|
|
import { Images } from './Images';
|
|
import { CreateInstance } from './CreateInstance';
|
|
vi.mock('./ProfileEditor', () => ({ProfileEditor: () => null}));
|
|
import { Instances } from './Instances';
|
|
const policy: Compatibility = {recipe_rev:'7', platform:'linux/amd64', entries:[], unknown:{state:'existing_only', build_allowed:false, reason:'unknown'}, experimental_admin_only:true};
|
|
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)];
|
|
}
|
|
function render(admin = false) {state.cursor = 0; return Images({admin});}
|
|
beforeEach(() => {state.slots = []; state.cursor = 0; vi.stubGlobal('window', {confirm:vi.fn(() => false)});});
|
|
afterEach(() => {vi.restoreAllMocks(); vi.unstubAllGlobals();});
|
|
it('hides experimental and admission controls from ordinary users', () => {
|
|
const tree = render();
|
|
expect(nodes(tree, n => n.type === 'input' && n.props.type === 'checkbox')).toHaveLength(0);
|
|
expect(nodes(tree, n => n.type === 'button' && n.props.children === '准入管理')).toHaveLength(0);
|
|
expect(nodes(render(true), n => n.type === 'button' && n.props.children === '准入管理')).toHaveLength(2);
|
|
});
|
|
it('canceled build confirmation sends zero requests', () => {
|
|
const request = vi.spyOn(api, 'startBuild');
|
|
const check = vi.spyOn(api, 'compatibility');
|
|
nodes(render(true), n => n.type === 'form')[0].props.onSubmit({preventDefault:vi.fn()});
|
|
expect(window.confirm).toHaveBeenCalledOnce();
|
|
expect(request).not.toHaveBeenCalled(); expect(check).not.toHaveBeenCalled();
|
|
});
|
|
it('requires evidence and confirmation; cancel sends no admission mutation', () => {
|
|
const request = vi.spyOn(api, 'admitImage');
|
|
nodes(render(true), n => n.type === 'button' && n.props.children === '准入管理')[0].props.onClick();
|
|
const child = nodes(render(true), n => typeof n.type === 'function' && n.props.imageId)[0];
|
|
state.slots = []; state.cursor = 0;
|
|
let form = child.type(child.props);
|
|
expect(nodes(form, n => n.type === 'button')[0].props.disabled).toBe(true);
|
|
nodes(form, n => n.type === 'textarea')[0].props.onChange({target:{value:'实际验收记录'}});
|
|
nodes(form, n => n.type === 'input')[0].props.onChange({target:{checked:true}});
|
|
state.cursor = 0; form = child.type(child.props);
|
|
form.props.onSubmit({preventDefault:vi.fn()});
|
|
expect(window.confirm).toHaveBeenCalledOnce(); expect(request).not.toHaveBeenCalled();
|
|
});
|
|
it('unknown latest never reserves an intent, including a second submit', async () => {
|
|
const request = vi.spyOn(api, 'request').mockResolvedValue({version:'9.9.9', build_allowed:false});
|
|
vi.spyOn(api, 'compatibility').mockResolvedValue(policy);
|
|
vi.stubGlobal('FormData', class {get() {return 'name';}});
|
|
const renderCreate = () => {state.cursor = 0; return CreateInstance({close:vi.fn(), refresh:vi.fn(async () => {})});};
|
|
let form = renderCreate();
|
|
expect(nodes(form, n => n.type === 'option' && n.props.value === 'exp')).toHaveLength(0);
|
|
nodes(form, n => n.type === 'select')[0].props.onChange({target:{value:'latest'}});
|
|
form = renderCreate(); await form.props.onSubmit({preventDefault:vi.fn(), currentTarget:{}});
|
|
form = renderCreate(); await form.props.onSubmit({preventDefault:vi.fn(), currentTarget:{}});
|
|
expect(request.mock.calls).toEqual([['/latest']]);
|
|
});
|
|
it('admission rejects blank/unconfirmed evidence, resets consent on edits, and submits once', async () => {
|
|
const request = vi.spyOn(api, 'admitImage').mockResolvedValue({});
|
|
nodes(render(true), n => n.type === 'button' && n.props.children === '准入管理')[0].props.onClick();
|
|
const child = nodes(render(true), n => typeof n.type === 'function' && n.props.imageId)[0];
|
|
state.slots = [];
|
|
const renderAdmission = () => {state.cursor = 0; return child.type(child.props);};
|
|
const submit = () => renderAdmission().props.onSubmit({preventDefault:vi.fn()});
|
|
submit(); expect(request).not.toHaveBeenCalled();
|
|
nodes(renderAdmission(), n => n.type === 'input')[0].props.onChange({target:{checked:true}});
|
|
submit(); expect(request).not.toHaveBeenCalled();
|
|
nodes(renderAdmission(), n => n.type === 'textarea')[0].props.onChange({target:{value:'验收记录 #42'}});
|
|
expect(nodes(renderAdmission(), n => n.type === 'input')[0].props.checked).toBe(false);
|
|
submit(); expect(request).not.toHaveBeenCalled();
|
|
nodes(renderAdmission(), n => n.type === 'input')[0].props.onChange({target:{checked:true}});
|
|
nodes(renderAdmission(), n => n.type === 'select')[0].props.onChange({target:{value:'recommended'}});
|
|
expect(nodes(renderAdmission(), n => n.type === 'input')[0].props.checked).toBe(false);
|
|
nodes(renderAdmission(), n => n.type === 'input')[0].props.onChange({target:{checked:true}});
|
|
vi.mocked(window.confirm).mockReturnValue(true);
|
|
submit(); submit();
|
|
await Promise.resolve();
|
|
expect(request).toHaveBeenCalledExactlyOnceWith('ok', 'recommended', '验收记录 #42');
|
|
});
|
|
it.each([
|
|
['a'.repeat(4096), 4096, true],
|
|
['a'.repeat(4097), 4097, false],
|
|
['中'.repeat(1400), 4200, false],
|
|
['中'.repeat(1365), 4095, true],
|
|
['中'.repeat(1365) + 'a', 4096, true],
|
|
['中'.repeat(1365) + 'ab', 4097, false],
|
|
['😀'.repeat(1024), 4096, true],
|
|
['😀'.repeat(1024) + 'a', 4097, false],
|
|
[' ' + 'é'.repeat(2048) + '\n', 4096, true],
|
|
] as const)('checks UTF-8 evidence bytes (case %#)', async (evidence, bytes, valid) => {
|
|
const request = vi.spyOn(api, 'admitImage').mockResolvedValue({});
|
|
vi.mocked(window.confirm).mockReturnValue(true);
|
|
nodes(render(true), n => n.type === 'button' && n.props.children === '准入管理')[0].props.onClick();
|
|
const child = nodes(render(true), n => typeof n.type === 'function' && n.props.imageId)[0];
|
|
state.slots = [];
|
|
const renderAdmission = () => {state.cursor = 0; return child.type(child.props);};
|
|
nodes(renderAdmission(), n => n.type === 'textarea')[0].props.onChange({target:{value:evidence}});
|
|
nodes(renderAdmission(), n => n.type === 'input')[0].props.onChange({target:{checked:true}});
|
|
const form = renderAdmission();
|
|
expect(nodes(form, n => n.type === 'textarea')[0].props.maxLength).toBeUndefined();
|
|
expect(nodes(form, n => n.type === 'textarea')[0].props['aria-invalid']).toBe(!valid);
|
|
expect(nodes(form, n => n.type === 'p' && Array.isArray(n.props.children) && n.props.children.includes(bytes))).toHaveLength(1);
|
|
expect(nodes(form, n => n.type === 'button')[0].props.disabled).toBe(!valid);
|
|
form.props.onSubmit({preventDefault:vi.fn()});
|
|
await Promise.resolve();
|
|
if (valid) expect(request).toHaveBeenCalledExactlyOnceWith('ok', 'compatible', evidence.trim());
|
|
else {expect(request).not.toHaveBeenCalled(); expect(window.confirm).not.toHaveBeenCalled();}
|
|
});
|
|
it('upgrade selector excludes experimental and unadmitted images', () => {
|
|
state.cursor = 0; Instances();
|
|
state.slots = [];
|
|
// Instances keeps the selected detail after its action state and creating flag.
|
|
state.slots[5] = {id:'instance', name:'test', version:'1', status:'stopped'};
|
|
state.cursor = 0;
|
|
const detail = nodes(Instances(), n => typeof n.type === 'function' && n.props.instance)[0];
|
|
state.slots = ['upgrade']; state.cursor = 0;
|
|
const tree = detail.type(detail.props);
|
|
expect(nodes(tree, n => n.type === 'option' && n.props.value === 'ok')).toHaveLength(1);
|
|
expect(nodes(tree, n => n.type === 'option' && n.props.value === 'exp')).toHaveLength(0);
|
|
});
|