This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { flushSync, mount, unmount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import DiscordPage from '../../src/routes/settings/integrations/discord/+page.svelte';
|
||||
import type { DiscordAdminState, DiscordSettings } from '../../src/lib/discord.js';
|
||||
|
||||
const page = writable({ data: { user: { role: 'admin' } } });
|
||||
vi.mock('$app/stores', () => ({
|
||||
page: { subscribe: (run: Parameters<typeof page.subscribe>[0]) => page.subscribe(run) }
|
||||
}));
|
||||
|
||||
let component: ReturnType<typeof mount> | undefined;
|
||||
beforeEach(() => {
|
||||
vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible');
|
||||
});
|
||||
afterEach(async () => {
|
||||
if (component) await unmount(component);
|
||||
component = undefined;
|
||||
document.body.replaceChildren();
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const serverName = 't5_zombie_theater_1';
|
||||
const guildId = '12345678901234567';
|
||||
const channelId = '23456789012345678';
|
||||
function snapshot(): DiscordAdminState {
|
||||
return {
|
||||
settings: {
|
||||
enabled: false,
|
||||
guildId,
|
||||
channels: { t5: channelId, t6: '' },
|
||||
publishedServers: [],
|
||||
showPlayers: false,
|
||||
showAddresses: false,
|
||||
leaderboardEnabled: false
|
||||
},
|
||||
credentials: {
|
||||
applicationId: '34567890123456789',
|
||||
publicKeyConfigured: true,
|
||||
botTokenConfigured: true
|
||||
},
|
||||
status: {
|
||||
lastAttemptAt: null,
|
||||
lastSuccessAt: null,
|
||||
lastError: null,
|
||||
boards: [],
|
||||
activeSubscriptions: 0,
|
||||
pendingNotifications: 0
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
name: serverName,
|
||||
game: 't5',
|
||||
label: 'Kino der Toten · Lobby 1',
|
||||
map: 'Kino der Toten'
|
||||
}
|
||||
],
|
||||
preview: { t5: [], t6: [] }
|
||||
};
|
||||
}
|
||||
|
||||
const text = (node: Element) => node.textContent?.replace(/\s+/g, ' ').trim();
|
||||
const findButton = (label: string) =>
|
||||
[...document.querySelectorAll('button')].find((node) => text(node) === label);
|
||||
const button = (label: string) => {
|
||||
const node = findButton(label);
|
||||
expect(node, `Missing button: ${label}`).toBeDefined();
|
||||
return node!;
|
||||
};
|
||||
const checkbox = (label: string) => {
|
||||
const node = [...document.querySelectorAll('label')].find((node) =>
|
||||
text(node)?.includes(label)
|
||||
);
|
||||
const input = node?.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
||||
expect(input, `Missing checkbox: ${label}`).toBeTruthy();
|
||||
return input!;
|
||||
};
|
||||
const field = (label: string) => {
|
||||
const node = [...document.querySelectorAll('label')].find((node) => text(node) === label)!;
|
||||
const input = document.getElementById(node.htmlFor) as HTMLInputElement;
|
||||
expect(input?.tagName).toBe('INPUT');
|
||||
return input;
|
||||
};
|
||||
function enter(label: string, value: string) {
|
||||
const input = field(label);
|
||||
input.value = value;
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
flushSync();
|
||||
}
|
||||
function toggle(label: string) {
|
||||
checkbox(label).click();
|
||||
flushSync();
|
||||
}
|
||||
async function show(role = 'admin') {
|
||||
page.set({ data: { user: { role } } });
|
||||
component = mount(DiscordPage, { target: document.body });
|
||||
flushSync();
|
||||
await vi.waitFor(() => {
|
||||
flushSync();
|
||||
expect(document.querySelector('form')).not.toBeNull();
|
||||
});
|
||||
}
|
||||
|
||||
it('saves explicit publication choices without credentials and restores saved values on discard', async () => {
|
||||
const initial = snapshot();
|
||||
let submitted: DiscordSettings | undefined;
|
||||
const fetch = vi.fn(async (_url: string, init?: RequestInit) => {
|
||||
if (init?.method === 'PUT') {
|
||||
submitted = JSON.parse(String(init.body)) as DiscordSettings;
|
||||
return Response.json({ ...initial, settings: submitted });
|
||||
}
|
||||
return Response.json(initial);
|
||||
});
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
await show();
|
||||
|
||||
expect(checkbox('Enable Discord companion').checked).toBe(false);
|
||||
expect(checkbox(serverName).checked).toBe(false);
|
||||
expect(checkbox('Show player names').checked).toBe(false);
|
||||
expect(checkbox('Show public server connection addresses').checked).toBe(false);
|
||||
expect(checkbox('Publish fleet leaderboards').checked).toBe(false);
|
||||
expect(document.body.textContent).toContain('including servers outside the selection above');
|
||||
|
||||
toggle('Enable Discord companion');
|
||||
toggle(serverName);
|
||||
toggle('Show player names');
|
||||
toggle('Publish fleet leaderboards');
|
||||
enter('Discord server ID', '45678901234567890');
|
||||
expect(button('Register commands').disabled).toBe(true);
|
||||
button('Save changes').click();
|
||||
await vi.waitFor(() => expect(document.body.textContent).toContain('Settings saved.'));
|
||||
|
||||
expect(submitted).toEqual({
|
||||
...initial.settings,
|
||||
enabled: true,
|
||||
guildId: '45678901234567890',
|
||||
publishedServers: [serverName],
|
||||
showPlayers: true,
|
||||
leaderboardEnabled: true
|
||||
});
|
||||
expect(fetch.mock.calls.filter(([, init]) => init?.method === 'PUT')).toHaveLength(1);
|
||||
expect(fetch.mock.calls.every(([url]) => url === '/api/integrations/discord')).toBe(true);
|
||||
expect(button('Save changes').disabled).toBe(true);
|
||||
|
||||
toggle('Show player names');
|
||||
toggle(serverName);
|
||||
button('Discard').click();
|
||||
flushSync();
|
||||
expect(checkbox('Show player names').checked).toBe(true);
|
||||
expect(checkbox(serverName).checked).toBe(true);
|
||||
expect(button('Save changes').disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps Super Viewer controls read-only and rejects a directly submitted mutation', async () => {
|
||||
const fetch = vi.fn(async (_url: string, _init?: RequestInit) => Response.json(snapshot()));
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
await show('super_viewer');
|
||||
|
||||
expect(document.body.textContent).toContain('You have read-only access.');
|
||||
for (const group of document.querySelectorAll('fieldset')) expect(group.disabled).toBe(true);
|
||||
expect(findButton('Save changes')).toBeUndefined();
|
||||
expect(findButton('Register commands')).toBeUndefined();
|
||||
document
|
||||
.querySelector('form')!
|
||||
.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
flushSync();
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
button('Refresh status').click();
|
||||
await vi.waitFor(() => expect(fetch).toHaveBeenCalledTimes(2));
|
||||
expect(fetch.mock.calls.every((args) => !(args[1] as RequestInit | undefined)?.method)).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves publication edits and the last snapshot across polling failures and recovery', async () => {
|
||||
vi.useFakeTimers();
|
||||
const initial = snapshot();
|
||||
const updated = snapshot();
|
||||
updated.status.activeSubscriptions = 3;
|
||||
const fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(Response.json(initial))
|
||||
.mockRejectedValueOnce(new Error('Discord status unavailable.'))
|
||||
.mockResolvedValueOnce(Response.json(updated));
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
await show();
|
||||
toggle(serverName);
|
||||
enter('Discord server ID', '45678901234567890');
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
flushSync();
|
||||
expect(document.querySelector('[role="alert"]')?.textContent).toContain(
|
||||
'Discord status unavailable.'
|
||||
);
|
||||
expect(document.body.textContent).toContain('Kino der Toten · Lobby 1');
|
||||
expect(checkbox(serverName).checked).toBe(true);
|
||||
expect(field('Discord server ID').value).toBe('45678901234567890');
|
||||
|
||||
button('Retry loading').click();
|
||||
await vi.waitFor(() => expect(document.querySelector('[role="alert"]')).toBeNull());
|
||||
expect(checkbox(serverName).checked).toBe(true);
|
||||
expect(field('Discord server ID').value).toBe('45678901234567890');
|
||||
expect(button('Save changes').disabled).toBe(false);
|
||||
expect(document.body.textContent).toContain('Unsaved changes');
|
||||
});
|
||||
|
||||
it('registers commands only on an explicit action and retains its error through successful refreshes', async () => {
|
||||
const fetch = vi.fn(async (url: string) =>
|
||||
url.endsWith('/register')
|
||||
? Response.json({ error: 'Command registration denied.' }, { status: 502 })
|
||||
: Response.json(snapshot())
|
||||
);
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
await show();
|
||||
expect(fetch.mock.calls.some(([url]) => url.endsWith('/register'))).toBe(false);
|
||||
button('Register commands').click();
|
||||
await vi.waitFor(() =>
|
||||
expect(document.body.textContent).toContain('Command registration denied.')
|
||||
);
|
||||
await vi.waitFor(() => expect(fetch).toHaveBeenCalledTimes(3));
|
||||
expect(fetch.mock.calls[1]).toEqual(['/api/integrations/discord/register', { method: 'POST' }]);
|
||||
button('Refresh status').click();
|
||||
await vi.waitFor(() => expect(fetch).toHaveBeenCalledTimes(4));
|
||||
expect(document.querySelector('[role="alert"]')?.textContent).toContain(
|
||||
'Command registration denied.'
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user