import { afterEach, beforeEach, expect, it, vi } from 'vitest'; const state = vi.hoisted(() => ({slots: [] as any[], cursor: 0})); vi.mock('react', async original => ({ ...await original(), 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(), 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); });