Require release versions for script uploads
CI / checks (push) Successful in 5m26s

This commit is contained in:
2026-10-12 18:46:57 -04:00
parent ffda04f276
commit 2f52dcb1ef
50 changed files with 871 additions and 113 deletions
+147
View File
@@ -0,0 +1,147 @@
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<typeof mount> | 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<HTMLInputElement>('input[placeholder="1.2.0"]')];
expect(versions).toHaveLength(3);
const uploadButton = [...document.querySelectorAll<HTMLButtonElement>('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<HTMLInputElement>('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<HTMLInputElement>('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<HTMLButtonElement>('button')].find(
(button) => button.textContent?.trim() === 'Review update'
)!;
expect(review.disabled).toBe(true);
const version = document.querySelector<HTMLInputElement>('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<HTMLButtonElement>('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);
});