import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; import { renderToStaticMarkup } from 'react-dom/server'; import { DeleteProgressToast } from '../components/common/DeleteProgressToast'; import { apiClient } from './api-client'; import { beginDeleteTask, getDeleteProgressSnapshot, resetDeleteProgress, subscribeDeleteProgress, trackDelete, } from './delete-progress'; test('delete progress store reports the oldest label and notifies subscribers', () => { resetDeleteProgress(); let notifications = 0; const unsubscribe = subscribeDeleteProgress(() => { notifications += 1; }); try { assert.deepEqual(getDeleteProgressSnapshot(), { count: 0, label: null }); const endFirst = beginDeleteTask('Deleting session'); const endSecond = beginDeleteTask('Deleting episode'); assert.deepEqual(getDeleteProgressSnapshot(), { count: 2, label: 'Deleting session' }); assert.equal(notifications, 2); endFirst(); assert.deepEqual(getDeleteProgressSnapshot(), { count: 1, label: 'Deleting episode' }); endFirst(); assert.deepEqual( getDeleteProgressSnapshot(), { count: 1, label: 'Deleting episode' }, 'ending the same task twice must not drop another task', ); endSecond(); assert.deepEqual(getDeleteProgressSnapshot(), { count: 0, label: null }); } finally { unsubscribe(); resetDeleteProgress(); } }); test('trackDelete clears the indicator even when the request rejects', async () => { resetDeleteProgress(); await assert.rejects( trackDelete('Deleting library entry', async () => { assert.equal(getDeleteProgressSnapshot().count, 1); throw new Error('boom'); }), /boom/, ); assert.deepEqual(getDeleteProgressSnapshot(), { count: 0, label: null }); }); test('DeleteProgressToast stays hidden while idle and reports active deletes', () => { resetDeleteProgress(); assert.equal(renderToStaticMarkup(), ''); const end = beginDeleteTask('Deleting library entry'); try { const markup = renderToStaticMarkup(); assert.match(markup, /role="status"/); assert.match(markup, /Deleting library entry/); assert.match(markup, /animate-indeterminate/); } finally { end(); } const endFirst = beginDeleteTask('Deleting session'); const endSecond = beginDeleteTask('Deleting episode'); try { assert.match(renderToStaticMarkup(), /Deleting 2 items/); } finally { endFirst(); endSecond(); resetDeleteProgress(); } }); test('the delete indicator is mounted once at the app root, not inside tab panels', () => { const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const read = (relativePath: string): string => fs.readFileSync(path.join(srcDir, relativePath), 'utf8'); const app = read('App.tsx'); assert.match(app, //); // Sibling of the confirm dialog: outside every `hidden` tab panel, so the // indicator survives tab switches and detail-view navigation. assert.match(app, /\s*/); for (const tab of [ 'components/overview/OverviewTab.tsx', 'components/sessions/SessionsTab.tsx', ]) { assert.doesNotMatch(read(tab), /DeleteProgressToast/, `${tab} must not mount its own toast`); } }); test('every api client delete registers with the global progress indicator', async () => { const originalFetch = globalThis.fetch; const seenCounts: number[] = []; globalThis.fetch = (async () => { seenCounts.push(getDeleteProgressSnapshot().count); return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } }); }) as typeof globalThis.fetch; try { resetDeleteProgress(); await apiClient.deleteSession(1); await apiClient.deleteSessions([1, 2]); await apiClient.deleteVideo(3); await apiClient.deleteAnime(4); assert.deepEqual(seenCounts, [1, 1, 1, 1]); assert.deepEqual(getDeleteProgressSnapshot(), { count: 0, label: null }); } finally { globalThis.fetch = originalFetch; resetDeleteProgress(); } });