import { afterEach, expect, it, vi } from 'vitest'; import { flushSync, mount, unmount } from 'svelte'; import BetaRevisionDialog from '../../src/lib/ui/components/BetaRevisionDialog.svelte'; import ScriptUploadDrawer from '../../src/lib/ui/components/ScriptUploadDrawer.svelte'; vi.mock('$app/navigation', () => ({ goto: vi.fn() })); let component: ReturnType | undefined; afterEach(async () => { if (component) await unmount(component); component = undefined; document.body.replaceChildren(); vi.unstubAllGlobals(); }); it('fills exact filenames from a manifest, preserves manual values, and sends each release', async () => { const uploaded: FormData[] = []; vi.stubGlobal( 'fetch', vi.fn(async (url: string, options?: RequestInit) => { if (url === '/api/scope-options') return Response.json({ games: [], mapsByGame: {} }); if (url === '/api/servers') return Response.json({ servers: [] }); if (url === '/api/scripts/inspect') return Response.json({ sha256: 'test', duplicates: [] }); uploaded.push(options!.body as FormData); return Response.json({ ok: true, script: { id: uploaded.length }, versionId: uploaded.length }); }) ); const drawer = mount(ScriptUploadDrawer, { target: document.body, props: { scripts: [], onuploaded: async () => {} } }); component = drawer; flushSync(); const dialog = document.querySelector('dialog')!; dialog.showModal = () => { dialog.open = true; }; await drawer.addFiles([ new File(['hud'], 'hud.gsc'), new File(['join'], 'join.gsc'), new File(['lock'], 'lock.gsc') ]); flushSync(); const versions = [...document.querySelectorAll('input[placeholder="1.2.0"]')]; expect(versions).toHaveLength(3); const uploadButton = [...document.querySelectorAll('button')].find( (button) => button.textContent?.trim() === 'Upload' )!; expect(uploadButton.disabled).toBe(true); versions[0].value = '3.0.0'; versions[0].dispatchEvent(new Event('input', { bubbles: true })); flushSync(); const manifest = document.querySelector('input[accept=".json"]')!; Object.defineProperty(manifest, 'files', { configurable: true, value: [ new File(['{"hud.gsc":"1.0.0","join.gsc":"2.0.0","LOCK.gsc":"9.0.0"}'], 'versions.json') ] }); manifest.dispatchEvent(new Event('change', { bubbles: true })); await vi.waitFor(() => { flushSync(); expect(versions[1].value).toBe('2.0.0'); }); expect(versions[0].value).toBe('3.0.0'); expect(versions[2].value).toBe(''); expect(uploadButton.disabled).toBe(true); versions[2].value = '1.0'; versions[2].dispatchEvent(new Event('input', { bubbles: true })); flushSync(); expect(uploadButton.disabled).toBe(true); versions[2].value = '1.0.0'; versions[2].dispatchEvent(new Event('input', { bubbles: true })); flushSync(); expect(uploadButton.disabled).toBe(false); uploadButton.click(); await vi.waitFor(() => expect(uploaded).toHaveLength(3)); expect(uploaded.map((form) => form.get('releaseVersion'))).toEqual(['3.0.0', '2.0.0', '1.0.0']); }); it('requires a replacement release before beta review and includes it in the revision request', async () => { let submitted: FormData | null = null; const onUpdate = vi.fn(); vi.stubGlobal( 'fetch', vi.fn(async (url: string, options?: RequestInit) => { if (url === '/api/scripts/inspect') return Response.json({ sha256: 'new-hash', duplicates: [] }); if (url.endsWith('/files/4')) return new Response('original script'); submitted = options!.body as FormData; return Response.json({ ok: true }); }) ); component = mount(BetaRevisionDialog, { target: document.body, props: { open: true, betaId: 7, revision: 1, servers: [], onUpdate, files: [ { id: 4, name: 'HUD', kind: 'script', target_rel_path: 'hud.gsc', version: { version: 1, release_version: '1.0.0', sha256: 'old-hash' } } ] } }); flushSync(); const input = document.querySelector('input[type="file"]:not([accept])')!; Object.defineProperty(input, 'files', { configurable: true, value: [new File(['updated script'], 'hud.gsc')] }); input.dispatchEvent(new Event('change', { bubbles: true })); await vi.waitFor(() => { flushSync(); expect(document.querySelector('input[required]')).not.toBeNull(); }); const review = [...document.querySelectorAll('button')].find( (button) => button.textContent?.trim() === 'Review update' )!; expect(review.disabled).toBe(true); const version = document.querySelector('input[required]')!; version.value = '1.0.1'; version.dispatchEvent(new Event('input', { bubbles: true })); flushSync(); expect(review.disabled).toBe(false); review.click(); flushSync(); expect(document.body.textContent).toContain('Release 1.0.0 → 1.0.1'); const deploy = [...document.querySelectorAll('button')].find( (button) => button.textContent?.trim() === 'Deploy revision 2' )!; deploy.click(); await vi.waitFor(() => expect(onUpdate).toHaveBeenCalledOnce()); expect(submitted!.get('release:4')).toBe('1.0.1'); expect(submitted!.get('file:4')).toBeInstanceOf(File); });