Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -1,21 +1,48 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { LaunchPreview } from '../../src/lib/deployment-plan.js';
|
||||
import { createServerLaunchController, type ServerLaunchInput, type ServerLaunchState } from '../../src/lib/ui/server-launch.js';
|
||||
import {
|
||||
createServerLaunchController,
|
||||
type ServerLaunchInput,
|
||||
type ServerLaunchState
|
||||
} from '../../src/lib/ui/server-launch.js';
|
||||
|
||||
const selection: ServerLaunchInput = { game: 't5', mapSlug: 'kino', libraryId: 'library-1', image: 'runtime:latest' };
|
||||
const selection: ServerLaunchInput = {
|
||||
game: 't5',
|
||||
mapSlug: 'kino',
|
||||
libraryId: 'library-1',
|
||||
image: 'runtime:latest'
|
||||
};
|
||||
const response = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status });
|
||||
const preview = (id = 'preview-1', expiresIn = 60_000): LaunchPreview => ({
|
||||
id, expiresAt: new Date(Date.now() + expiresIn).toISOString(), name: 't5_zombie_theater_1',
|
||||
port: null, hasSavedKey: true, blueprintName: null, checks: [], ready: true,
|
||||
recipe: { game: 't5', mapSlug: 'kino', mapName: 'Kino Der Toten', expectedMap: 'zombie_theater',
|
||||
capturedAt: new Date().toISOString(), image: 'runtime:latest', configFile: 'server.cfg',
|
||||
configSource: 'T5 Zombies', files: [], fingerprint: 'recipe-1' },
|
||||
id,
|
||||
expiresAt: new Date(Date.now() + expiresIn).toISOString(),
|
||||
name: 't5_zombie_theater_1',
|
||||
port: null,
|
||||
hasSavedKey: true,
|
||||
blueprintName: null,
|
||||
checks: [],
|
||||
ready: true,
|
||||
recipe: {
|
||||
game: 't5',
|
||||
mapSlug: 'kino',
|
||||
mapName: 'Kino Der Toten',
|
||||
expectedMap: 'zombie_theater',
|
||||
capturedAt: new Date().toISOString(),
|
||||
image: 'runtime:latest',
|
||||
configFile: 'server.cfg',
|
||||
configSource: 'T5 Zombies',
|
||||
files: [],
|
||||
fingerprint: 'recipe-1'
|
||||
}
|
||||
});
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (error: Error) => void;
|
||||
const promise = new Promise<T>((done, fail) => { resolve = done; reject = fail; });
|
||||
const promise = new Promise<T>((done, fail) => {
|
||||
resolve = done;
|
||||
reject = fail;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
@@ -29,10 +56,16 @@ beforeEach(() => {
|
||||
vi.setSystemTime(new Date('2026-10-07T18:00:00Z'));
|
||||
state = { preview: null, checking: false, error: '' };
|
||||
fetchPreview = vi.fn<typeof fetch>();
|
||||
changes = vi.fn((next: ServerLaunchState) => { state = next; });
|
||||
changes = vi.fn((next: ServerLaunchState) => {
|
||||
state = next;
|
||||
});
|
||||
controller = createServerLaunchController(changes, { fetch: fetchPreview });
|
||||
});
|
||||
afterEach(() => { controller.dispose(); vi.useRealTimers(); vi.restoreAllMocks(); });
|
||||
afterEach(() => {
|
||||
controller.dispose();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('automatic server launch checks', () => {
|
||||
it('debounces rapid selections and sends only the final game, map, and native overrides', async () => {
|
||||
@@ -46,7 +79,11 @@ describe('automatic server launch checks', () => {
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(fetchPreview).toHaveBeenCalledTimes(1);
|
||||
expect(fetchPreview.mock.calls[0][0]).toBe('/api/provisioning/preview');
|
||||
expect(JSON.parse(fetchPreview.mock.calls[0][1]!.body as string)).toEqual({ ...selection, mapSlug: 'five', publicPort: 30100 });
|
||||
expect(JSON.parse(fetchPreview.mock.calls[0][1]!.body as string)).toEqual({
|
||||
...selection,
|
||||
mapSlug: 'five',
|
||||
publicPort: 30100
|
||||
});
|
||||
expect(state.preview?.id).toBe('preview-1');
|
||||
expect(state.checking).toBe(false);
|
||||
});
|
||||
@@ -78,26 +115,33 @@ describe('automatic server launch checks', () => {
|
||||
expect(state.preview?.id).toBe('preview-1');
|
||||
});
|
||||
|
||||
it.each(['success', 'error'] as const)('aborts changed requests and ignores stale %s responses', async (result) => {
|
||||
const first = deferred<Response>();
|
||||
fetchPreview.mockReturnValueOnce(first.promise).mockResolvedValueOnce(response(preview('new-preview')));
|
||||
controller.select(selection);
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
const signal = fetchPreview.mock.calls[0][1]!.signal!;
|
||||
controller.select({ ...selection, mapSlug: 'five' });
|
||||
expect(signal.aborted).toBe(true);
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
if (result === 'success') first.resolve(response(preview('old-preview')));
|
||||
else first.reject(new Error('Old network error'));
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(state.preview?.id).toBe('new-preview');
|
||||
expect(state.error).toBe('');
|
||||
expect(state.checking).toBe(false);
|
||||
});
|
||||
it.each(['success', 'error'] as const)(
|
||||
'aborts changed requests and ignores stale %s responses',
|
||||
async (result) => {
|
||||
const first = deferred<Response>();
|
||||
fetchPreview
|
||||
.mockReturnValueOnce(first.promise)
|
||||
.mockResolvedValueOnce(response(preview('new-preview')));
|
||||
controller.select(selection);
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
const signal = fetchPreview.mock.calls[0][1]!.signal!;
|
||||
controller.select({ ...selection, mapSlug: 'five' });
|
||||
expect(signal.aborted).toBe(true);
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
if (result === 'success') first.resolve(response(preview('old-preview')));
|
||||
else first.reject(new Error('Old network error'));
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(state.preview?.id).toBe('new-preview');
|
||||
expect(state.error).toBe('');
|
||||
expect(state.checking).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it('ignores a response from a dismissed session after reopening the same selection', async () => {
|
||||
const first = deferred<Response>();
|
||||
fetchPreview.mockReturnValueOnce(first.promise).mockResolvedValueOnce(response(preview('reopened-preview')));
|
||||
fetchPreview
|
||||
.mockReturnValueOnce(first.promise)
|
||||
.mockResolvedValueOnce(response(preview('reopened-preview')));
|
||||
controller.select(selection);
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
controller.clear();
|
||||
@@ -120,7 +164,9 @@ describe('automatic server launch checks', () => {
|
||||
|
||||
it('refreshes immediately and never retains an old preview if the refresh fails', async () => {
|
||||
const refresh = deferred<Response>();
|
||||
fetchPreview.mockResolvedValueOnce(response(preview())).mockReturnValueOnce(refresh.promise);
|
||||
fetchPreview
|
||||
.mockResolvedValueOnce(response(preview()))
|
||||
.mockReturnValueOnce(refresh.promise);
|
||||
controller.select(selection);
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
controller.refresh();
|
||||
@@ -128,7 +174,11 @@ describe('automatic server launch checks', () => {
|
||||
expect(state).toEqual({ preview: null, checking: true, error: '' });
|
||||
refresh.resolve(response({ error: 'No runtime is available.' }, 409));
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(state).toEqual({ preview: null, checking: false, error: 'No runtime is available.' });
|
||||
expect(state).toEqual({
|
||||
preview: null,
|
||||
checking: false,
|
||||
error: 'No runtime is available.'
|
||||
});
|
||||
fetchPreview.mockResolvedValueOnce(response(preview('retry-preview')));
|
||||
controller.refresh();
|
||||
expect(state.error).toBe('');
|
||||
@@ -138,7 +188,9 @@ describe('automatic server launch checks', () => {
|
||||
|
||||
it('automatically renews an expired preview and removes it while checking', async () => {
|
||||
const renewal = deferred<Response>();
|
||||
fetchPreview.mockResolvedValueOnce(response(preview('initial', 1000))).mockReturnValueOnce(renewal.promise);
|
||||
fetchPreview
|
||||
.mockResolvedValueOnce(response(preview('initial', 1000)))
|
||||
.mockReturnValueOnce(renewal.promise);
|
||||
controller.select(selection);
|
||||
await vi.advanceTimersByTimeAsync(999);
|
||||
expect(state.preview?.id).toBe('initial');
|
||||
@@ -155,7 +207,11 @@ describe('automatic server launch checks', () => {
|
||||
controller.select(selection);
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
expect(fetchPreview).toHaveBeenCalledTimes(1);
|
||||
expect(state).toEqual({ preview: null, checking: false, error: 'The launch check expired. Try again.' });
|
||||
expect(state).toEqual({
|
||||
preview: null,
|
||||
checking: false,
|
||||
error: 'The launch check expired. Try again.'
|
||||
});
|
||||
});
|
||||
|
||||
it('freezes a preview during submission and resumes checks after a failed launch', async () => {
|
||||
|
||||
Reference in New Issue
Block a user