This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { flushSync, mount, tick, unmount } from 'svelte';
|
||||
import { fromStore, writable } from 'svelte/store';
|
||||
import BankPage from '../../src/routes/bank/+page.svelte';
|
||||
import { readBankQuery, type BankOverview } from '../../src/lib/bank.js';
|
||||
|
||||
const navigation = vi.hoisted(() => ({ goto: vi.fn(), before: [] as (() => void)[] }));
|
||||
const currentPage = writable({ url: new URL('http://localhost/bank'), data: { user: null } });
|
||||
vi.mock('$app/stores', () => ({
|
||||
page: {
|
||||
subscribe: (run: Parameters<typeof currentPage.subscribe>[0]) => currentPage.subscribe(run)
|
||||
},
|
||||
navigating: writable(null)
|
||||
}));
|
||||
vi.mock('$app/navigation', () => ({
|
||||
goto: navigation.goto,
|
||||
beforeNavigate: (fn: () => void) => navigation.before.push(fn),
|
||||
afterNavigate: vi.fn()
|
||||
}));
|
||||
vi.mock('../../src/lib/ui/poll.js', () => ({ pollVisible: () => () => {} }));
|
||||
|
||||
let component: ReturnType<typeof mount> | undefined;
|
||||
beforeEach(() => {
|
||||
navigation.goto.mockReset().mockResolvedValue(undefined);
|
||||
navigation.before.length = 0;
|
||||
});
|
||||
afterEach(async () => {
|
||||
if (component) await unmount(component);
|
||||
component = undefined;
|
||||
document.body.replaceChildren();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const account = {
|
||||
guid: '18446744073709551615',
|
||||
name: 'Dempsey',
|
||||
balance: 2000,
|
||||
available: 1500,
|
||||
pendingOperation: 'withdraw' as const,
|
||||
pendingAmount: 500,
|
||||
lastActivity: '2026-10-13T11:00:00.000Z',
|
||||
lastAmount: 2000
|
||||
};
|
||||
const transfer = {
|
||||
id: 1,
|
||||
guid: account.guid,
|
||||
name: account.name,
|
||||
amount: 2000,
|
||||
balance: 2000,
|
||||
server: 't5_zombie_theater_1',
|
||||
game: 't5',
|
||||
matchId: 17,
|
||||
matchAvailable: 1,
|
||||
createdAt: account.lastActivity
|
||||
};
|
||||
function report(search = ''): BankOverview {
|
||||
const query = readBankQuery(new URLSearchParams(search));
|
||||
return {
|
||||
query,
|
||||
maximum: 250000,
|
||||
checkedAt: '2026-10-13T12:00:00.000Z',
|
||||
summary: { totalBanked: 2000, fundedAccounts: 1, transfers24h: 1, pending: 1 },
|
||||
servers: [transfer.server],
|
||||
accounts: { entries: [account], total: 1, offset: 0 },
|
||||
transactions: { entries: [transfer], total: 1, offset: 0 },
|
||||
pending: {
|
||||
entries: [{ ...transfer, requestId: 'pending-id', amount: 500, operation: 'withdraw' }],
|
||||
total: 1,
|
||||
offset: 0
|
||||
},
|
||||
detail: query.player
|
||||
? {
|
||||
account,
|
||||
pending: null,
|
||||
history: { entries: [transfer], total: 30, offset: query.historyOffset }
|
||||
}
|
||||
: null
|
||||
};
|
||||
}
|
||||
function show(initial = report()) {
|
||||
const data = writable({ report: initial, user: null, setupRequired: false });
|
||||
const state = fromStore(data);
|
||||
currentPage.set({
|
||||
url: new URL(
|
||||
`http://localhost/bank?${new URLSearchParams(Object.entries(initial.query).map(([key, value]) => [key, String(value)]))}`
|
||||
),
|
||||
data: { user: null }
|
||||
});
|
||||
component = mount(BankPage, {
|
||||
target: document.body,
|
||||
props: {
|
||||
get data() {
|
||||
return state.current;
|
||||
}
|
||||
}
|
||||
});
|
||||
flushSync();
|
||||
return (next: BankOverview) => {
|
||||
currentPage.set({
|
||||
url: new URL(
|
||||
`http://localhost/bank?${new URLSearchParams(Object.entries(next.query).map(([key, value]) => [key, String(value)]))}`
|
||||
),
|
||||
data: { user: null }
|
||||
});
|
||||
data.set({ report: next, user: null, setupRequired: false });
|
||||
flushSync();
|
||||
};
|
||||
}
|
||||
const button = (label: string) =>
|
||||
[...document.querySelectorAll('button')].find((node) => node.textContent?.trim() === label)!;
|
||||
const lastNavigation = () => new URL(navigation.goto.mock.calls.at(-1)![0], 'http://localhost');
|
||||
|
||||
it('opens player details without losing list filters and restores the list when closed', async () => {
|
||||
const update = show(report('q=Dempsey&sort=recent&offset=25'));
|
||||
button('Dempsey').click();
|
||||
expect(lastNavigation().searchParams.get('player')).toBe(account.guid);
|
||||
expect(lastNavigation().searchParams.get('q')).toBe('Dempsey');
|
||||
expect(lastNavigation().searchParams.get('offset')).toBe('25');
|
||||
update(report(`q=Dempsey&sort=recent&offset=25&player=${account.guid}`));
|
||||
await tick();
|
||||
expect(document.querySelector('dialog')?.open).toBe(true);
|
||||
expect(document.querySelector('dialog')?.textContent).toContain('1,500 pts');
|
||||
const profile = [...document.querySelectorAll('dialog a')].find((link) =>
|
||||
link.textContent?.includes('View player profile')
|
||||
)!;
|
||||
const target = new URL(profile.getAttribute('href')!, 'http://localhost');
|
||||
expect(target.searchParams.get('returnTo')).toContain(`player=${account.guid}`);
|
||||
button('Next').click();
|
||||
expect(lastNavigation().searchParams.get('historyOffset')).toBe('25');
|
||||
expect(lastNavigation().searchParams.get('offset')).toBe('25');
|
||||
button('Close').click();
|
||||
expect(lastNavigation().searchParams.has('player')).toBe(false);
|
||||
expect(lastNavigation().searchParams.get('sort')).toBe('recent');
|
||||
});
|
||||
|
||||
it('applies accessible transfer filters and resets pagination while preserving player detail', () => {
|
||||
show(report(`view=transactions&offset=25&player=${account.guid}`));
|
||||
const search = document.querySelector<HTMLInputElement>('input[name="q"]')!;
|
||||
search.value = 'Nikolai';
|
||||
search.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
const game = document.querySelector<HTMLSelectElement>('select[name="game"]')!;
|
||||
// happy-dom's :checked selector only matches inputs, not selected options.
|
||||
const selectQuery = game.querySelector.bind(game);
|
||||
game.querySelector = ((selector: string) =>
|
||||
selector === ':checked'
|
||||
? (game.selectedOptions[0] ?? null)
|
||||
: selectQuery(selector)) as typeof game.querySelector;
|
||||
game.value = 't6';
|
||||
game.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
flushSync();
|
||||
document
|
||||
.querySelector('form')!
|
||||
.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
expect(lastNavigation().searchParams.get('q')).toBe('Nikolai');
|
||||
expect(lastNavigation().searchParams.get('game')).toBe('t6');
|
||||
expect(lastNavigation().searchParams.has('offset')).toBe(false);
|
||||
expect(lastNavigation().searchParams.get('player')).toBe(account.guid);
|
||||
button('Reset').click();
|
||||
expect(lastNavigation().searchParams.has('game')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows pending reservation guidance, age and historical matches without dead links', () => {
|
||||
const initial = report('view=pending');
|
||||
initial.pending.entries[0].matchAvailable = 0;
|
||||
show(initial);
|
||||
const panel = document.querySelector('#bank-results')!;
|
||||
expect(panel.textContent).toContain('Waiting over 5 minutes');
|
||||
expect(panel.textContent).toContain('These points are reserved');
|
||||
expect(panel.textContent).toContain('Match #17 · History expired');
|
||||
expect(panel.querySelector('a[href*="matches/17"]')).toBeNull();
|
||||
expect(panel.querySelector('details summary')?.textContent).toBe('Why pending?');
|
||||
});
|
||||
|
||||
it('retains data on refresh errors and successfully retries without erasing draft filters', async () => {
|
||||
const fetch = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('offline'))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify(report())));
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
show();
|
||||
const search = document.querySelector<HTMLInputElement>('input[name="q"]')!;
|
||||
search.value = 'draft';
|
||||
search.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
button('Refresh').click();
|
||||
await vi.waitFor(() => expect(document.body.textContent).toContain('Bank updates paused.'));
|
||||
expect(document.querySelector('#bank-results')?.textContent).toContain('Dempsey');
|
||||
button('Retry').click();
|
||||
await vi.waitFor(() => expect(document.body.textContent).not.toContain('Bank updates paused.'));
|
||||
expect(search.value).toBe('draft');
|
||||
});
|
||||
|
||||
it('discards an old refresh when navigation changes the bank view', async () => {
|
||||
let resolve!: (response: Response) => void;
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(
|
||||
() =>
|
||||
new Promise<Response>((done) => {
|
||||
resolve = done;
|
||||
})
|
||||
)
|
||||
);
|
||||
const update = show();
|
||||
button('Refresh').click();
|
||||
navigation.before.forEach((fn) => fn());
|
||||
const next = report('view=transactions');
|
||||
next.summary.totalBanked = 9000;
|
||||
update(next);
|
||||
resolve(new Response(JSON.stringify(report())));
|
||||
await tick();
|
||||
await tick();
|
||||
expect(document.querySelector('.summary')?.textContent).toContain('9,000');
|
||||
expect(document.querySelector('#bank-tab-transactions')?.getAttribute('aria-selected')).toBe(
|
||||
'true'
|
||||
);
|
||||
expect(document.querySelector('#bank-results')?.textContent).toContain('Balance after');
|
||||
});
|
||||
Reference in New Issue
Block a user