// Generated, do not edit. sha256:2927734ac98d4252a0f01084377081712c0c677a75cd3c6e74946090a662fa75
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { installDom } from '../helpers/mini-dom.mjs';
import {
cellClasses, cellHtml, expCellHtml, headRowHtml, controlsHtml, actionsCellHtml, editActionsHtml, groupHeaderRowHtml, frameHtml,
} from '../../client/ui/table-markup.js';
const dom = installDom();
const labels = { new: 'New', save: 'Save', cancel: 'Cancel', none: 'Ungrouped', search: 'Search', count: 'Records', edit: 'Edit' };
const parse = html => { const el = dom.host(); el.innerHTML = html; return el; };
const row = cells => parse(`
`);
describe('cells', () => {
it('classes: alignment and text, in that order, and nothing for a plain column', () => {
assert.deepEqual(cellClasses({ align: 'num', text: true }), ['num', 'text']);
assert.deepEqual(cellClasses({ align: 'center' }), ['center']);
assert.deepEqual(cellClasses({}), []);
});
it('a value without render is escaped; a render is markup; a text column clamps; data-label is escaped', () => {
const td = row(cellHtml({ key: 'n', label: 'A "b"', text: true }, { n: 'x ' })).querySelector('td');
assert.equal(td.querySelector('.cell-clamp').textContent, 'x ');
assert.equal(td.querySelector('i'), null);
assert.equal(td.dataset.label, 'A "b"');
const r = row(cellHtml({ key: 'n', render: v => `${v.n} ` }, { n: 1 })).querySelector('td b');
assert.equal(r.textContent, '1');
});
it('a missing value is empty, not "undefined" or "null"', () => {
assert.equal(row(cellHtml({ key: 'n' }, { n: null })).querySelector('td').textContent, '');
assert.equal(row(cellHtml({ key: 'n' }, {})).querySelector('td').textContent, '');
});
it('the chevron cell only in a table with a drawer, turned when open', () => {
assert.equal(expCellHtml(false, true, 'v'), '');
const b = row(expCellHtml(true, true, ' ')).querySelector('.chev-btn');
assert.ok(b.classList.contains('is-open'));
assert.equal(b.getAttribute('aria-hidden'), 'true');
});
});
describe('the head', () => {
const columns = [{ key: 'a', label: 'A', sortable: true, align: 'num' }, { key: 'b', label: 'B' }];
it('sortable headers carry data-sort and th-sort; the active one an arrow per direction', () => {
const head = parse(`${headRowHtml({ columns, sort: { key: 'a', dir: 'desc' }, hasExpansion: false, hasActions: false })}
`);
const th = head.querySelector('th[data-sort="a"]');
assert.deepEqual(th.className.split(' '), ['num', 'th-sort']);
assert.equal(th.querySelector('.sort-arrow').textContent, '▼');
assert.equal(head.querySelectorAll('th[data-sort]').length, 1);
});
it('an empty header cell for the chevron and for the actions, only when there are', () => {
const with_ = parse(`${headRowHtml({ columns, sort: null, hasExpansion: true, hasActions: true })}
`);
assert.equal(with_.querySelectorAll('th').length, 4);
const without = parse(`${headRowHtml({ columns, sort: null, hasExpansion: false, hasActions: false })}
`);
assert.equal(without.querySelectorAll('th').length, 2);
});
});
describe('the controls', () => {
const groupCols = [{ key: 'g', label: 'G', group: {} }, { key: 'h', label: 'H', group: {} }, { key: 'i', label: 'I', group: {} }];
const base = { cfg: { rows: [1, 2], columns: groupCols }, labels, canCreate: true, groupCols, groupKey: null, groupKey2: null, groupKey3: null, search: '', editing: false };
const ctl = extra => parse(controlsHtml({ ...base, ...extra, cfg: { ...base.cfg, ...(extra.cfg || {}) } }));
it('the plus, the search box and the count; no group select deeper than chosen', () => {
const c = ctl({});
assert.ok(c.querySelector('[data-tbl-new]'));
assert.equal(c.querySelector('[data-tbl-new]').title, 'New');
assert.equal(c.querySelector('[data-tbl-search]').getAttribute('placeholder'), 'Search');
assert.equal(c.querySelector('.tbl-count').title, 'Records');
assert.equal(c.querySelector('[id]'), null, 'no ids: a page may hold two tables');
assert.equal(c.querySelector('.tbl-count').textContent, '2');
assert.equal(c.querySelectorAll('.tbl-group-select').length, 1);
assert.equal(c.querySelector('[data-gfilter]'), null);
});
it('without canCreate no plus, and a toolbarNote function is read and escaped', () => {
const c = ctl({ canCreate: false, cfg: { toolbarNote: () => '1 1' } });
assert.equal(c.querySelector('[data-tbl-new]'), null);
assert.equal(c.querySelector('.tbl-note').textContent, '1 1');
});
it('three levels: the second excludes the first, the third excludes both, and the filter slot appears', () => {
const c = ctl({ groupKey: 'g', groupKey2: 'h' });
assert.deepEqual(c.querySelector('[data-tbl-group="2"]').querySelectorAll('option').map(o => o.value), ['', 'h', 'i']);
assert.deepEqual(c.querySelector('[data-tbl-group="3"]').querySelectorAll('option').map(o => o.value), ['', 'i']);
assert.ok(c.querySelector('[data-gfilter]'));
});
it('groupChoice false: no select and no filter slot', () => {
const c = ctl({ groupKey: 'g', cfg: { groupChoice: false } });
assert.equal(c.querySelector('select'), null);
assert.equal(c.querySelector('[data-gfilter]'), null);
});
it('while editing everything is disabled; no style attribute anywhere', () => {
const html = controlsHtml({ ...base, groupKey: 'g', editing: true, cfg: { ...base.cfg, toolbarActions: [{ id: 'x', label: 'X' }] } });
const c = parse(html);
assert.ok(c.querySelectorAll('button, select, input').every(e => e.hasAttribute('disabled')));
assert.doesNotMatch(html, /style=/);
});
it('a toolbar action with an icon has the shape of the plus; without one it is a ghost button', () => {
const c = ctl({ cfg: { toolbarActions: [{ id: 'a', label: 'A', icon: ' ' }, { id: 'b', label: '' }] } });
assert.ok(c.querySelector('[data-tbl-act="a"].btn-new svg'));
assert.equal(c.querySelector('[data-tbl-act="b"].btn-ghost').textContent, '');
});
});
describe('the action cell', () => {
const cfg = { onSave() {}, onDelete() {}, extraActions: [{ key: 'x', label: 'X' }] };
const cellOf = args => row(actionsCellHtml({ cfg, labels, idKey: 'id', editing: false, isEditRow: false, ...args })).querySelector('td');
it('the edit row: an empty cell, save and cancel stand in the drawer (user, 26-10-2026)', () => {
assert.equal(cellOf({ row: null, isEditRow: true }).children.length, 0);
const bar = parse(editActionsHtml({ cfg: { saveLabel: '' }, labels }));
assert.equal(bar.querySelector('[data-save]').textContent, '');
assert.equal(bar.querySelector('[data-cancel]').textContent, 'Cancel');
});
it('empty on a row that is not selected, and on every row while another is edited', () => {
assert.equal(cellOf({ row: { id: 1 }, selectedId: 2 }).children.length, 0);
assert.equal(cellOf({ row: { id: 1 }, selectedId: 1, editing: true }).children.length, 0);
});
it('ids compare as text: a numeric selected id matches a string row id', () => {
assert.ok(cellOf({ row: { id: '1' }, selectedId: 1 }).querySelector('[data-edit="1"]'));
});
it('the selected row: extras, edit and delete, each only when the page allows it for this row', () => {
const td = cellOf({ row: { id: 1 }, selectedId: 1 });
assert.ok(td.querySelector('[data-action="x"]') && td.querySelector('[data-edit="1"]') && td.querySelector('[data-del="1"]'));
const locked = row(actionsCellHtml({ cfg: { ...cfg, rowEditable: () => false, rowDeletable: () => false }, labels, idKey: 'id', row: { id: 1 }, selectedId: 1, editing: false, isEditRow: false })).querySelector('td');
assert.equal(locked.querySelector('[data-edit]'), null);
assert.equal(locked.querySelector('[data-del]'), null);
});
});
describe('the group header and the frame', () => {
it('escapes the label, keeps the summary as markup, and is a button only when collapsible', () => {
const tr = parse(`${groupHeaderRowHtml({ token: 'a"b', label: '', count: 3, summaryHtml: 's ', isCollapsed: true, collapsible: true, colCount: 4, chevron: '' })}
`).querySelector('tr');
assert.ok(tr.classList.contains('is-collapsed'));
assert.equal(tr.querySelector('td').getAttribute('colspan'), '4');
assert.equal(tr.querySelector('.group-header > span').textContent, '');
assert.equal(tr.querySelector('.group-summary em').textContent, 's');
assert.equal(tr.querySelector('[data-gcollapse]').dataset.gcollapse, 'a"b');
const plain = parse(`${groupHeaderRowHtml({ token: 'a', label: 'A', count: 1, collapsible: false, colCount: 2, chevron: '' })}
`);
assert.equal(plain.querySelector('[data-gcollapse]'), null);
});
it('the frame: controls bar above the scrolling table, has-exp only with a drawer', () => {
const f = parse(frameHtml({ controls: ' ', head: ' ', body: '', hasExpansion: true }));
assert.ok(f.querySelector('.table-frame > .table-controls-bar > i'));
assert.ok(f.querySelector('.table-wrap.sticky-sel.has-exp > table > thead'));
assert.equal(parse(frameHtml({ controls: '', head: '', body: '', hasExpansion: false })).querySelector('.has-exp'), null);
});
it('heads the bar with an escaped title above the controls, and draws nothing without one', () => {
const html = frameHtml({ controls: ' ', head: '', body: '', hasExpansion: false, title: 'Coins & ' });
const bar = parse(html).querySelector('.table-controls-bar');
assert.ok(html.indexOf('tbl-title') < html.indexOf(''), 'the title comes first, the controls under it');
assert.equal(bar.querySelector('.tbl-title').textContent, 'Coins & ');
assert.equal(bar.querySelector('.tbl-title b, .tbl-title bars'), null, 'markup in a title is text');
assert.equal(parse(frameHtml({ controls: '', head: '', body: '', hasExpansion: false })).querySelector('.tbl-title'), null);
});
});