feat(library): add game links, auditor queue, audit log and filter
Build, Test & Push Frontend / quality-check (push) Successful in 25s
Build, Test & Push Frontend / docker (push) Successful in 23s

LinkGameDialog (user request or auditor link), EditionsCard on the
details page, LinkQueuePage and AuditLogPage for auditors with paging,
hideDuplicates filter and editions badge in the library, audit menu
entries, de/en texts.
This commit is contained in:
lobo committed 2026-10-02 09:10:56 +02:00
1 parent 80259cb0e9
commit f71e8f7aff
21 files changed
+992 -11

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
// @vitest-environment jsdom
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
import {MemoryRouter} from 'react-router-dom';
import LinkQueuePage from './LinkQueuePage.jsx';
import {PreferencesProvider} from '../context/PreferencesContext.jsx';
import {fetchRoutes, json} from '../test/fetchRoutes.js';
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
const pair = {a: {appid: 10, name: 'Dark Souls', imageUrl: null}, b: {appid: -7, name: 'Dark Souls Remastered', imageUrl: null},
igdb: true, requests: 2, createdAt: '2026-10-01T10:00:00Z'};
const queue = (content, totalPages = 1) => json(200, {content, page: 0, size: 20, totalElements: content.length, totalPages});
const renderPage = (routes) => {
fetchRoutes(routes);
render(<MemoryRouter><PreferencesProvider><LinkQueuePage/></PreferencesProvider></MemoryRouter>);
};
beforeEach(() => {
localStorage.setItem('tbp_session', '1');
});
afterEach(() => {
cleanup();
localStorage.clear();
vi.clearAllMocks();
});
describe('LinkQueuePage', () => {
it('listsThePairWithItsBadges', async () => {
renderPage({'GET /api/library/link-queue': queue([pair])});
expect(await screen.findByText('Dark Souls')).toBeTruthy();
expect(screen.getByText('Dark Souls Remastered')).toBeTruthy();
expect(screen.getByText('LINKS.FROM_IGDB')).toBeTruthy();
expect(screen.getByText('LINKS.REQUESTED_BY:2')).toBeTruthy();
});
it('linksThePairAndLoadsTheQueueAgain', async () => {
let pending = [pair];
renderPage({
'GET /api/library/link-queue': () => queue(pending),
'POST /api/library/games/10/link': () => {
pending = [];
return json(204);
},
});
fireEvent.click(await screen.findByRole('button', {name: 'LINKS.LINK'}));
expect(await screen.findByText('LINKS.QUEUE_EMPTY')).toBeTruthy();
const post = fetch.mock.calls.find(([, init]) => init?.method === 'POST');
expect(JSON.parse(post[1].body)).toEqual({otherAppid: -7});
});
it('dismissesThePair', async () => {
renderPage({
'GET /api/library/link-queue': queue([pair]),
'DELETE /api/library/link-queue/10/-7': json(204),
});
fireEvent.click(await screen.findByRole('button', {name: 'LINKS.DISMISS'}));
await waitFor(() => expect(fetch.mock.calls.some(([url, init]) => init?.method === 'DELETE' && url.endsWith('/link-queue/10/-7'))).toBe(true));
});
it('pagesThroughTheQueue', async () => {
renderPage({'GET /api/library/link-queue': () => queue([pair], 2)});
await screen.findByText('Dark Souls');
fireEvent.click(screen.getByRole('button', {name: /ADMIN.NEXT/}));
await waitFor(() => expect(fetch.mock.calls.at(-1)[0]).toContain('page=1'));
expect(screen.getByText('LINKS.PAGE_OF')).toBeTruthy();
});
it('goesBackAPageWhenThePageIsEmpty', async () => {
renderPage({'GET /api/library/link-queue': (url) => (url.includes('page=1') ? queue([], 2) : queue([pair], 2))});
await screen.findByText('Dark Souls');
fireEvent.click(screen.getByRole('button', {name: /ADMIN.NEXT/}));
await waitFor(() => expect(fetch.mock.calls.at(-1)[0]).toContain('page=0'));
expect(fetch.mock.calls.some(([url]) => url.includes('page=1'))).toBe(true);
expect(await screen.findByText('Dark Souls')).toBeTruthy();
});
it('showsNoAccessOn403', async () => {
renderPage({'GET /api/library/link-queue': json(403, {})});
expect(await screen.findByText('LINKS.NO_ACCESS')).toBeTruthy();
});
});