Files
yellowjacket/frontend/test/components/confirm-dialog.test.ts
T
logan 5830b1ba17 test(frontend): cover the lifecycle, the voice and the repaints
Component and store cases for everything in this series, several of
which exist because the thing they pin is invisible everywhere else:

- `view-lifecycle` and `keyboard-reach` — a document listener count
  that does not grow across a simulated navigate cycle, and a tab
  sequence that reaches the sidebar and plays a row without a mouse.
- `notifications`, `notification-store`, `confirm-dialog`,
  `empty-states` — the four levels, the (level, region, key)
  coalescing window, and loading/failed/empty as three states.
- `card-grid-repaint` — fails if `artists-view`'s or `genres-view`'s
  per-render arrow functions are hoisted to stable fields, which is
  the audit's own recommendation and takes the cards from 1 highlighted
  to 0. It exists for no other reason.
- `lazy-track-details` — reads the five sources and fails on a
  returning static import, the same shape as `TestNoDirectRuntimeEmits`
  and for the same reason: the invariant is about what the code does
  *not* say.
- `now-playing` — a position report that changes nothing must not
  touch the DOM again, and a track change must. The first fails
  against the old unconditional `updated()`.
- `playlist-virtualization`, `list-render-cost`, `selection`, `icons`,
  and the store cases for the library-filter race, the never-settling
  waiter and the per-playlist patch.
2026-08-12 01:20:03 -04:00

83 lines
2.5 KiB
TypeScript

/**
* The one "are you sure?".
*
* Three destructive actions had none at all — including a multi-select
* loop that deleted N playlists on one click (errors.M6, M7, m4) — so
* what matters here is that the promise a call site awaits cannot
* resolve true unless somebody said so.
*/
import { describe, expect, it } from 'vitest';
import {
confirmAction,
type ConfirmDialog,
} from '@components/confirm-dialog/confirm-dialog';
/** The singleton the helper attaches to the document on first use. */
function host(): ConfirmDialog {
const el = document.querySelector<ConfirmDialog>('confirm-dialog');
if (!el) throw new Error('confirm-dialog did not mount itself');
return el;
}
async function press(testid: string): Promise<void> {
const el = host();
await el.updateComplete;
el.shadowRoot?.querySelector<HTMLButtonElement>(`[data-testid="${testid}"]`)
?.click();
await el.updateComplete;
}
describe('confirmAction', () => {
it('resolves true only when the user accepts', async () => {
const answer = confirmAction({
title: 'Delete “Chill”?',
message: 'The playlist is deleted; the audio files are not.',
confirmLabel: 'Delete playlist',
danger: true,
});
await press('confirm-accept');
await expect(answer).resolves.toBe(true);
});
it('resolves false on cancel, which is the default answer', async () => {
const answer = confirmAction({ title: 'Delete?', message: 'Gone for good.' });
await press('confirm-cancel');
await expect(answer).resolves.toBe(false);
});
it('says what will happen before asking', async () => {
const answer = confirmAction({
title: 'Remove “Lidarr”?',
message: 'YellowJacket will stop using this client.',
impact: 'Its stored credentials are deleted and cannot be recovered.',
});
const el = host();
await el.updateComplete;
expect(el.shadowRoot?.textContent).toContain('cannot be recovered');
await press('confirm-cancel');
await answer;
});
it('does not leave a second question hanging', async () => {
const first = confirmAction({ title: 'One?', message: 'First.' });
const second = confirmAction({ title: 'Two?', message: 'Second.' });
await press('confirm-accept');
// The first was superseded and answered "no" rather than never
// settling — a call site awaiting it would otherwise hang forever.
await expect(Promise.all([first, second])).resolves.toEqual([false, true]);
});
});