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
|
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.
|
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,
|
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
|
which defaults to 1 and applies to every game represented by the selected files
|
||||||
and their matching servers. Every automatically selected instance must pass
|
and their matching servers. Every automatically selected instance must pass
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
allowLeave = false;
|
allowLeave = false;
|
||||||
let hasLinkedSetup = $state(false);
|
let hasLinkedSetup = $state(false);
|
||||||
let loadingVersions = $state<Set<number>>(new Set());
|
let loadingVersions = $state<Set<number>>(new Set());
|
||||||
|
let addingAllFiles = $state(false);
|
||||||
const rowRequests = new Map<number, number>();
|
const rowRequests = new Map<number, number>();
|
||||||
let versionRequest = 0;
|
let versionRequest = 0;
|
||||||
let betaLabel = $state('');
|
let betaLabel = $state('');
|
||||||
@@ -103,6 +104,9 @@
|
|||||||
picks.some((pick) => pick.scriptId === file.id && versionsFor(pick).length > 0))
|
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(
|
const filesReady = $derived(
|
||||||
picks.length > 0 &&
|
picks.length > 0 &&
|
||||||
picks.every(
|
picks.every(
|
||||||
@@ -526,6 +530,22 @@
|
|||||||
loadingVersions = new Set([...loadingVersions].filter((key) => key !== pick.key));
|
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) {
|
function toggleServer(name: string) {
|
||||||
const next = new Set(pickedServers);
|
const next = new Set(pickedServers);
|
||||||
if (next.has(name)) next.delete(name);
|
if (next.has(name)) next.delete(name);
|
||||||
@@ -749,11 +769,21 @@
|
|||||||
</p>{/if}{/if}
|
</p>{/if}{/if}
|
||||||
</section>{/each}
|
</section>{/each}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div class="actions">
|
||||||
onclick={() =>
|
<Button
|
||||||
(picks = [...picks, { key: nextKey++, scriptId: null, versionId: null }])}
|
onclick={() =>
|
||||||
disabled={picks.length >= eligibleFiles.length}>Add file</Button
|
(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">
|
<p class="muted">
|
||||||
Combine files for different games in one beta. Each file installs only on
|
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.
|
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