This commit is contained in:
@@ -12,6 +12,10 @@ file applies to all participants. Every file needs at least one matching test
|
||||
participant. For example, a T5 script and a T6 script can share one beta without
|
||||
either script being installed on the other game's servers.
|
||||
|
||||
Choose **Add all eligible files** to add the latest unpromoted version of every
|
||||
remaining eligible file at once. Files and exact versions you already selected
|
||||
stay unchanged.
|
||||
|
||||
Setup defaults to automatic selection. Enter one **Instances per game** number,
|
||||
which defaults to 1 and applies to every game represented by the selected files
|
||||
and their matching servers. Every automatically selected instance must pass
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
allowLeave = false;
|
||||
let hasLinkedSetup = $state(false);
|
||||
let loadingVersions = $state<Set<number>>(new Set());
|
||||
let addingAllFiles = $state(false);
|
||||
const rowRequests = new Map<number, number>();
|
||||
let versionRequest = 0;
|
||||
let betaLabel = $state('');
|
||||
@@ -103,6 +104,9 @@
|
||||
picks.some((pick) => pick.scriptId === file.id && versionsFor(pick).length > 0))
|
||||
)
|
||||
);
|
||||
const remainingFiles = $derived(
|
||||
eligibleFiles.filter((file) => !picks.some((pick) => pick.scriptId === file.id))
|
||||
);
|
||||
const filesReady = $derived(
|
||||
picks.length > 0 &&
|
||||
picks.every(
|
||||
@@ -526,6 +530,22 @@
|
||||
loadingVersions = new Set([...loadingVersions].filter((key) => key !== pick.key));
|
||||
}
|
||||
}
|
||||
async function addAllFiles() {
|
||||
if (addingAllFiles || loadingVersions.size || !remainingFiles.length) return;
|
||||
const selected = picks.filter((pick) => pick.scriptId !== null);
|
||||
const additions = remainingFiles.map((file) => ({
|
||||
key: nextKey++,
|
||||
scriptId: file.id,
|
||||
versionId: null
|
||||
}));
|
||||
picks = [...selected, ...additions];
|
||||
addingAllFiles = true;
|
||||
try {
|
||||
await Promise.all(additions.map((_, index) => onPickFile(selected.length + index)));
|
||||
} finally {
|
||||
addingAllFiles = false;
|
||||
}
|
||||
}
|
||||
function toggleServer(name: string) {
|
||||
const next = new Set(pickedServers);
|
||||
if (next.has(name)) next.delete(name);
|
||||
@@ -749,11 +769,21 @@
|
||||
</p>{/if}{/if}
|
||||
</section>{/each}
|
||||
</div>
|
||||
<Button
|
||||
onclick={() =>
|
||||
(picks = [...picks, { key: nextKey++, scriptId: null, versionId: null }])}
|
||||
disabled={picks.length >= eligibleFiles.length}>Add file</Button
|
||||
>
|
||||
<div class="actions">
|
||||
<Button
|
||||
onclick={() =>
|
||||
(picks = [
|
||||
...picks,
|
||||
{ key: nextKey++, scriptId: null, versionId: null }
|
||||
])}
|
||||
disabled={picks.length >= eligibleFiles.length}>Add file</Button
|
||||
>
|
||||
<Button
|
||||
onclick={addAllFiles}
|
||||
disabled={!remainingFiles.length || loadingVersions.size > 0}
|
||||
loading={addingAllFiles}>Add all eligible files</Button
|
||||
>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Combine files for different games in one beta. Each file installs only on
|
||||
servers matching its scope, and every file needs at least one test participant.
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { flushSync, mount, unmount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import BetaSetupPage from '../../src/routes/betas/new/+page.svelte';
|
||||
|
||||
const currentPage = writable({
|
||||
url: new URL('http://localhost/betas/new'),
|
||||
data: { user: { role: 'operator' } }
|
||||
});
|
||||
vi.mock('$app/stores', () => ({
|
||||
page: {
|
||||
subscribe: (run: Parameters<typeof currentPage.subscribe>[0]) => currentPage.subscribe(run)
|
||||
}
|
||||
}));
|
||||
vi.mock('$app/navigation', () => ({ goto: vi.fn(), beforeNavigate: vi.fn() }));
|
||||
vi.mock('../../src/lib/ui/stores/fleet.js', () => ({
|
||||
fleetServers: writable([]),
|
||||
fleetError: writable(''),
|
||||
refreshFleet: vi.fn().mockResolvedValue(undefined)
|
||||
}));
|
||||
|
||||
let component: ReturnType<typeof mount> | undefined;
|
||||
beforeEach(() => {
|
||||
const storage = new Map<string, string>();
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => storage.set(key, value),
|
||||
removeItem: (key: string) => storage.delete(key)
|
||||
});
|
||||
window.history.replaceState({}, '', '/betas/new');
|
||||
});
|
||||
afterEach(async () => {
|
||||
if (component) await unmount(component);
|
||||
component = undefined;
|
||||
document.body.replaceChildren();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function file(
|
||||
id: number,
|
||||
options: {
|
||||
currentVersionId?: number;
|
||||
latestVersionId?: number | null;
|
||||
runningBetaId?: number;
|
||||
} = {}
|
||||
) {
|
||||
const latestVersionId =
|
||||
options.latestVersionId === undefined ? id * 10 + 2 : options.latestVersionId;
|
||||
return {
|
||||
id,
|
||||
name: `File ${id}`,
|
||||
kind: 'script',
|
||||
target_rel_path: `file-${id}.gsc`,
|
||||
scope_type: null,
|
||||
scope_value: null,
|
||||
current_version_id: options.currentVersionId ?? id * 10,
|
||||
latestVersion: latestVersionId === null ? null : { id: latestVersionId },
|
||||
runningBetaId: options.runningBetaId ?? null
|
||||
};
|
||||
}
|
||||
const files = [
|
||||
file(1),
|
||||
file(2),
|
||||
file(3, { currentVersionId: 32 }),
|
||||
file(4, { runningBetaId: 7 }),
|
||||
file(5, { latestVersionId: null })
|
||||
];
|
||||
const button = (label: string) =>
|
||||
[...document.querySelectorAll('button')].find((node) => node.textContent?.trim() === label)!;
|
||||
const selections = () =>
|
||||
[...document.querySelectorAll('.pick-row')].map((row) =>
|
||||
[...row.querySelectorAll('select')].map((select) => select.value)
|
||||
);
|
||||
|
||||
async function show({ query = '', failingFile = 0 } = {}) {
|
||||
window.history.replaceState({}, '', `/betas/new${query}`);
|
||||
currentPage.set({
|
||||
url: new URL(window.location.href),
|
||||
data: { user: { role: 'operator' } }
|
||||
});
|
||||
const fetch = vi.fn(async (url: string) => {
|
||||
if (url === '/api/scripts') return Response.json({ scripts: files });
|
||||
const script = files.find((entry) => url === `/api/scripts/${entry.id}`);
|
||||
if (!script) throw new Error(`Unexpected request: ${url}`);
|
||||
if (script.id === failingFile) return new Response('', { status: 503 });
|
||||
return Response.json({
|
||||
script,
|
||||
versions: [2, 1, 0].map((version) => ({
|
||||
id: script.id * 10 + version,
|
||||
version: version + 1,
|
||||
release_version: `1.0.${version}`,
|
||||
sha256: `hash-${script.id}-${version}`,
|
||||
state: 'untested'
|
||||
})),
|
||||
matchedServers: []
|
||||
});
|
||||
});
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
component = mount(BetaSetupPage, { target: document.body });
|
||||
flushSync();
|
||||
await vi.waitFor(() => {
|
||||
flushSync();
|
||||
expect(button('Add all eligible files')).toBeDefined();
|
||||
});
|
||||
return fetch;
|
||||
}
|
||||
|
||||
it('adds every eligible file with its latest unpromoted version and replaces empty rows', async () => {
|
||||
const fetch = await show();
|
||||
button('Add file').click();
|
||||
flushSync();
|
||||
expect(document.querySelectorAll('.pick-row')).toHaveLength(2);
|
||||
button('Add all eligible files').click();
|
||||
await vi.waitFor(() => {
|
||||
flushSync();
|
||||
expect(selections()).toEqual([
|
||||
['1', '12'],
|
||||
['2', '22']
|
||||
]);
|
||||
});
|
||||
expect(button('Continue').disabled).toBe(false);
|
||||
expect(button('Add all eligible files').disabled).toBe(true);
|
||||
button('Add all eligible files').click();
|
||||
flushSync();
|
||||
expect(document.querySelectorAll('.pick-row')).toHaveLength(2);
|
||||
expect(fetch.mock.calls.map(([url]) => url)).toEqual([
|
||||
'/api/scripts',
|
||||
'/api/scripts/1',
|
||||
'/api/scripts/2'
|
||||
]);
|
||||
});
|
||||
|
||||
it('preserves exact linked versions, including an older upload of a promoted file', async () => {
|
||||
await show({ query: '?artifact=1:11&artifact=3:31' });
|
||||
button('Add file').click();
|
||||
flushSync();
|
||||
button('Add all eligible files').click();
|
||||
await vi.waitFor(() => {
|
||||
flushSync();
|
||||
expect(selections()).toEqual([
|
||||
['1', '11'],
|
||||
['3', '31'],
|
||||
['2', '22']
|
||||
]);
|
||||
});
|
||||
expect(button('Continue').disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('retains successful selections and blocks continuing when one file cannot load versions', async () => {
|
||||
await show({ failingFile: 2 });
|
||||
button('Add all eligible files').click();
|
||||
await vi.waitFor(() => {
|
||||
flushSync();
|
||||
expect(document.querySelector('[role="alert"]')?.textContent).toContain(
|
||||
'Could not load versions'
|
||||
);
|
||||
});
|
||||
expect(selections()[0]).toEqual(['1', '12']);
|
||||
expect(selections()[1][0]).toBe('2');
|
||||
const failedVersion = document.querySelectorAll('.pick-row')[1].querySelectorAll('select')[1];
|
||||
expect(failedVersion.selectedOptions[0].textContent).toBe('Choose a version');
|
||||
expect(button('Continue').disabled).toBe(true);
|
||||
});
|
||||
Reference in New Issue
Block a user