Files
AtreidesAssetsFree/tests/ui/dialog.test.mjs
T
2026-10-29 11:57:29 +01:00

254 lines
11 KiB
JavaScript

// Generated, do not edit. sha256:768721f9fb6138508b5d90c78fcf36c2c24f85c154f27b169a0cb0109b7e699c
import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { installDom } from '../helpers/mini-dom.mjs';
const dom = installDom();
const dlg = await import('../../client/ui/dialog.js');
const overlays = () => dom.document.querySelectorAll('.dialog-overlay');
const top = () => overlays().at(-1);
const button = kind => top().querySelector(`[data-dlg="${kind}"]`);
const escape = () => dom.key(dom.document.activeElement || dom.document.body, 'Escape');
afterEach(() => {
for (let i = 0; i < 3 && overlays().length; i++) escape();
dlg.configureDialog();
});
describe('confirm, alert, notice', () => {
it('confirm resolves true on OK and false on cancel; OK has the focus', async () => {
const p = dlg.confirm({ title: 'Delete?', message: 'Sure <b>?</b>' });
assert.ok(dom.document.activeElement === button('ok'), 'OK has the focus');
assert.equal(top().querySelector('.dialog-body').textContent, 'Sure <b>?</b>');
dom.click(button('ok'));
assert.equal(await p, true);
const q = dlg.confirm({ message: 'x' });
dom.click(button('cancel'));
assert.equal(await q, false);
assert.equal(overlays().length, 0);
});
it('danger makes the confirm button red; labels can be given per call', async () => {
const p = dlg.confirm({ message: 'x', danger: true, confirmLabel: 'Weg', cancelLabel: 'Nee' });
assert.ok(button('ok').classList.contains('btn-danger'));
assert.equal(button('ok').textContent, 'Weg');
assert.equal(button('cancel').textContent, 'Nee');
escape();
assert.equal(await p, false);
});
it('a dialog WITH a button does not close on the backdrop; one without does', async () => {
const p = dlg.alert({ message: 'x' });
dom.click(top());
assert.equal(overlays().length, 1);
dom.click(button('ok'));
await p;
const n = dlg.notice({ message: 'restarting' });
assert.equal(top().querySelector('.dialog-actions'), null, 'no empty action block');
dom.click(top());
assert.equal(await n, false);
});
it('a second dialog replaces the first, which resolves false', async () => {
const p = dlg.confirm({ message: 'one' });
const q = dlg.confirm({ message: 'two' });
assert.equal(await p, false);
assert.equal(overlays().length, 1);
escape();
await q;
});
});
describe('the page behind', () => {
it('stops scrolling while a dialog is open, keeps the scrollbar width, and gets its own values back', async () => {
const root = dom.document.documentElement;
root.style.overflow = 'auto';
Object.defineProperty(root, 'clientWidth', { value: 1265, configurable: true });
const p = dlg.alert({ message: 'x' });
assert.equal(root.style.overflow, 'hidden');
assert.equal(root.style.paddingRight, '15px');
dom.click(button('ok'));
await p;
assert.equal(root.style.overflow, 'auto');
assert.equal(root.style.paddingRight, '');
delete root.clientWidth;
});
});
describe('form', () => {
const fields = [
{ key: 'name', label: 'Naam', value: 'a"b' },
{ key: 'kind', label: 'Soort', type: 'select', value: 'y', options: [{ value: 'x', label: 'X' }, { value: 'y', label: 'Y' }] },
{ key: 'note', label: 'Notitie', type: 'textarea' },
];
it('gives the values on OK, with the first field focused and values escaped into the markup', async () => {
const p = dlg.form({ title: 'New', fields });
const name = top().querySelector('[data-dlg-field="name"]');
assert.ok(dom.document.activeElement === name);
assert.equal(name.value, 'a"b');
assert.equal(top().querySelector('[data-dlg-field="kind"]').value, 'y');
name.value = 'c';
dom.click(button('ok'));
assert.deepEqual(await p, { name: 'c', kind: 'y', note: '' });
});
it('Enter in an input confirms; Enter in a textarea does not; cancel gives a falsy value', async () => {
const p = dlg.form({ fields });
dom.key(top().querySelector('textarea'), 'Enter');
assert.equal(overlays().length, 1);
dom.key(top().querySelector('input'), 'Enter');
assert.ok(await p);
const q = dlg.form({ fields });
dom.click(top());
assert.equal(overlays().length, 1, 'the backdrop does not throw the input away');
dom.click(button('cancel'));
assert.equal(await q, null);
});
});
describe('detail and what stacks on it', () => {
it('a wide panel with a close cross, no actions, and the markup as given', async () => {
const p = dlg.detail({ title: 'Asset', html: '<p class="x">body</p>', size: 'xl', asideHtml: '<em>tag</em>' });
const d = top().querySelector('.dialog');
assert.ok(d.classList.contains('dialog--wide') && d.classList.contains('dialog--xl'));
assert.ok(d.querySelector('.dialog-close'));
assert.equal(d.querySelector('.dialog-actions'), null);
assert.equal(d.querySelector('p.x').textContent, 'body');
assert.equal(d.querySelector('[data-dialog-aside] em').textContent, 'tag');
dom.click(top());
assert.equal(overlays().length, 1, 'the cross is a way out, so the backdrop does not close');
dom.click(d.querySelector('.dialog-close'));
assert.equal(await p, false);
});
it('a confirmation from inside stacks on top; Escape closes only the top one', async () => {
const p = dlg.detail({ title: 'Asset', html: '' });
const q = dlg.confirm({ message: 'Delete this row?' });
assert.equal(overlays().length, 2);
assert.ok(top().classList.contains('dialog-overlay--stacked'));
assert.equal(dlg.isDialogOpen(overlays()[0].querySelector('.dialog')), true, 'the stacked one is in the way');
escape();
assert.equal(await q, false);
assert.equal(overlays().length, 1);
escape();
assert.equal(await p, false);
});
it('alert and form stack too, and a stacked form gives its values', async () => {
const p = dlg.detail({ title: 'x', html: '' });
const a = dlg.alert({ message: 'm' });
dom.click(button('ok'));
assert.equal(await a, true);
const f = dlg.form({ fields: [{ key: 'k', label: 'K', value: 'v' }] });
dom.key(top().querySelector('input'), 'Enter');
assert.deepEqual(await f, { k: 'v' });
assert.equal(overlays().length, 1);
escape();
await p;
});
it('Escape in a field of a detail panel belongs to that field, the next one closes the panel', async () => {
const p = dlg.detail({ title: 'x', html: '<input class="f">' });
const f = top().querySelector('input.f');
dom.key(f, 'Escape');
assert.equal(overlays().length, 1);
dom.key(top().querySelector('.dialog'), 'Escape');
assert.equal(await p, false);
});
it('isDialogOpen: an element inside the open dialog is not under it, one outside is', async () => {
assert.equal(dlg.isDialogOpen(), false);
const p = dlg.detail({ title: 'x', html: '<span class="in"></span>' });
assert.equal(dlg.isDialogOpen(top().querySelector('.in')), false);
assert.equal(dlg.isDialogOpen(dom.document.body), true);
escape();
await p;
});
it('onDetail gets the dialog element and the promise of its closing', async () => {
let got = null;
dlg.configureDialog({ onDetail: (d, done) => { got = { cls: d.className, done }; d.querySelector('.dialog-title').append(dom.document.createElement('b')); } });
const p = dlg.detail({ title: 'x', html: '' });
assert.equal(got.cls, 'dialog dialog--wide');
assert.ok(top().querySelector('.dialog-title b'));
assert.equal(got.done, p);
escape();
await p;
});
it('confirmOver without an open dialog is an ordinary confirm', async () => {
const p = dlg.confirmOver({ message: 'x' });
assert.equal(overlays().length, 1);
dom.click(button('ok'));
assert.equal(await p, true);
});
});
describe('stepper', () => {
const steps = [
{ title: 'Kind', html: () => '<button data-step-set="kind" data-value="btc">btc</button><button data-step-set="kind" data-value="all">all</button>', ready: s => !!s.kind },
{ title: 'Only bitcoin?', html: () => '<button data-step-toggle="tags" data-value="a">a</button><button data-step-toggle="tags" data-value="b">b</button><button data-step-toggle="tags" data-value="c">c</button>', skip: s => s.kind !== 'btc' },
{ title: 'Done', html: () => '<p>ok</p>' },
];
const next = () => top().querySelector('[data-step="next"]');
it('next stays disabled until the step is ready; a set choice fills the state', async () => {
const p = dlg.stepper({ title: 'Setup', steps });
assert.ok(next().hasAttribute('disabled'));
dom.click(next());
assert.equal(top().querySelector('.stepper-title').textContent, 'Kind', 'a disabled next does nothing');
dom.click(top().querySelector('[data-value="btc"]'));
assert.equal(next().hasAttribute('disabled'), false);
dom.click(next());
assert.equal(top().querySelector('.stepper-title').textContent, 'Only bitcoin?');
for (const v of ['a', 'b', 'c', 'a']) dom.click(top().querySelector(`[data-step-toggle][data-value="${v}"]`));
dom.click(next());
assert.equal(next().textContent, 'Finish');
dom.click(next());
assert.deepEqual(await p, { kind: 'btc', tags: ['b', 'c'] }, 'a toggle adds, a second click takes it out');
});
it('draws the hero once: a step change keeps the same element, so its animation does not replay', async () => {
const p = dlg.stepper({ steps, hero: '<div class="logo"></div>' });
const hero = top().querySelector('.stepper-hero .logo');
assert.ok(hero);
dom.click(top().querySelector('[data-value="btc"]'));
dom.click(next());
assert.equal(top().querySelector('.stepper-hero .logo'), hero);
dom.click(top().querySelector('[data-step="skip"]'));
assert.equal(await p, false);
});
it('a skipped step is left out, back goes back, and skip gives false', async () => {
const p = dlg.stepper({ steps, state: { kind: 'all' } });
dom.click(next());
assert.equal(top().querySelector('.stepper-title').textContent, 'Done');
dom.click(top().querySelector('[data-step="back"]'));
assert.equal(top().querySelector('.stepper-title').textContent, 'Kind');
dom.click(top().querySelector('[data-step="skip"]'));
assert.equal(await p, false);
});
});
describe('configureDialog', () => {
it('sets the button texts and the close icon; unset fields keep the default', async () => {
dlg.configureDialog({ labels: { ok: 'Goed', cancel: 'Annuleren', close: 'Sluiten' }, closeIcon: '<i class="x"></i>' });
const p = dlg.confirm({ message: 'x' });
assert.equal(button('ok').textContent, 'Goed');
assert.equal(button('cancel').textContent, 'Annuleren');
escape();
await p;
const d = dlg.detail({ title: 'x', html: '' });
assert.equal(top().querySelector('.dialog-close').getAttribute('aria-label'), 'Sluiten');
assert.ok(top().querySelector('.dialog-close i.x'));
escape();
await d;
const s = dlg.stepper({ steps: [{ html: () => '' }] });
assert.equal(top().querySelector('[data-step="skip"]').textContent, 'Skip');
escape();
await s;
});
});