feat(library): add the merge duplicate dialog
Build, Test & Push Frontend / quality-check (push) Successful in 18s
Build, Test & Push Frontend / docker (push) Successful in 24s

This commit is contained in:
lobo committed 2026-09-28 11:25:45 +02:00
1 parent 01ae372b2d
commit 78ba0700c8
10 files changed
+355 -84

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
// @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 {toast} from 'sonner';
import GameMergeDialog from './GameMergeDialog.jsx';
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn()}}));
const hades = {appid: 1145360, name: 'Hades'};
const hit = (appid, name) => ({appid, igdbId: Math.abs(appid), name, year: 2020, imageUrl: null,
platforms: ['Switch'], inLibrary: false, inCatalog: true});
const json = (status, body) => ({ok: status < 400, status, json: async () => body});
function renderDialog(game, post, results) {
globalThis.fetch = vi.fn(async (url, init) => init?.method === 'POST'
? post
: json(200, {available: true, results}));
const onMerged = vi.fn();
render(<MemoryRouter><GameMergeDialog game={game} open onOpenChange={vi.fn()} onMerged={onMerged}/></MemoryRouter>);
return onMerged;
}
const search = (value) => fireEvent.change(screen.getByRole('searchbox'), {target: {value}});
beforeEach(() => {
localStorage.setItem('tbp_session', '1');
});
afterEach(() => {
cleanup();
localStorage.clear();
vi.clearAllMocks();
});
describe('GameMergeDialog', () => {
it('merges the picked game after the confirm step', async () => {
const onMerged = renderDialog(hades, json(200, {appid: 1145360}),
[hit(1145360, 'Hades'), hit(-113112, 'Hades (Switch)'), hit(1145350, 'Hades II'),
{...hit(-4711, 'Hades Fan Remake'), inCatalog: false}]);
search('hades');
const offered = await screen.findByRole('button', {name: /Hades \(Switch\)/});
// The page's own game, other Steam games and games not in ToBePlayed are not offered.
expect(screen.queryByRole('button', {name: /^Hades \(2020\)/})).toBeNull();
expect(screen.queryByRole('button', {name: /Hades II/})).toBeNull();
expect(screen.queryByRole('button', {name: /Fan Remake/})).toBeNull();
fireEvent.click(offered);
expect(fetch).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByRole('button', {name: 'MERGE.CONFIRM_BUTTON'}));
await waitFor(() => expect(onMerged).toHaveBeenCalledWith(1145360));
const [url, init] = fetch.mock.calls[1];
expect(url).toContain('/api/library/games/1145360/merge');
expect(JSON.parse(init.body)).toEqual({otherAppid: -113112});
expect(toast.success).toHaveBeenCalledWith('MERGE.DONE');
});
it('goes back from the confirm step without merging', async () => {
renderDialog(hades, json(200, {}), [hit(-113112, 'Hades (Switch)')]);
search('hades');
fireEvent.click(await screen.findByRole('button', {name: /Hades \(Switch\)/}));
fireEvent.click(screen.getByRole('button', {name: 'MERGE.BACK'}));
expect(await screen.findByRole('searchbox')).toBeTruthy();
expect(fetch).toHaveBeenCalledTimes(1);
});
it('says why Core refused the merge', async () => {
const onMerged = renderDialog({appid: -113112, name: 'Hades (Switch)'},
json(404, {code: 'GAME_NOT_FOUND'}), [hit(1145360, 'Hades')]);
search('hades');
fireEvent.click(await screen.findByRole('button', {name: /Hades \(2020\)/}));
fireEvent.click(screen.getByRole('button', {name: 'MERGE.CONFIRM_BUTTON'}));
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('MERGE.ERROR.GAME_NOT_FOUND'));
expect(onMerged).not.toHaveBeenCalled();
});
});