import fs from 'fs'; import path from 'path'; /* * The map pin table's action cells. * * Each row offers edit, add-to-playlist and delete, and they were laid out as bare siblings in * a table cell. That makes them inline-block boxes aligned on their text BASELINES, and a * Button's baseline is not an ActionIcon's -- measured on the running app, the middle control * sat 4.3px above the other two -- and they touched, because JSX leaves no whitespace between * elements. `Group` is what every other row of buttons in the app uses. * * Checked in the source because neither row is reachable from a test that could measure it: * `MapPins` fetches its pins from the API and builds its rows through SortableTable, and the * row builders are internal to the module. ui-layout.cy.js measures the pattern itself -- * that bare siblings really are misaligned and that a Group really does fix it -- so what is * left to guard is that these cells use one. */ const source = fs.readFileSync(path.join(__dirname, 'Map.js'), 'utf8'); /** The contents of every `` in the file. */ const cells = () => { const found = []; const open = '', at); if (end !== -1) found.push(source.slice(at, end)); } return found; }; describe('the map pin action cells', () => { it('finds the cells at all', () => { // The premise. A parser that matched nothing would report no offenders and read // exactly like a file that was already correct. expect(cells().length).toBeGreaterThan(5); }); /* * Named cells, not a count of control tags. * * Counting was tried first and does not work: a cell whose buttons are the two arms of a * ternary renders exactly one of them and needs no Group -- the "Built" / "Build" cell on * the map files table is that shape -- and excluding ternaries to allow for it also * excluded the subscribe cell, whose `role={isSubscribed ? ... : ...}` has nothing to do * with how many controls it holds. A guard that quietly stops covering a cell is worse * than one with a stated scope. * * So each cell is named by something only it contains, and the general claim -- that bare * inline controls really are misaligned and that a Group really does fix it -- is measured * in ui-layout.cy.js instead. */ const rows = [ {name: 'pin actions', marker: 'setEditingId(pin.id)'}, /* * `role='cancel'`, not `onSave(...)`: the edit row's TextInput calls onSave from its * own onKeyDown, in a different cell, so that marker matched the input's cell and this * quietly checked the wrong one. A marker has to be unique to the cell it names. */ {name: 'pin edit save/cancel', marker: "role='cancel'"}, {name: 'map file subscribe', marker: 'setPreviewOpen(true)'}, ]; rows.forEach(({name, marker}) => { it(`groups the ${name} cell`, () => { const cell = cells().find(candidate => candidate.includes(marker)); /* * The premise and the claim in one object, because jest's `expect` takes no * message argument -- that is chai, and passing one throws rather than annotating. * A rename of the marker would otherwise turn this into a test of nothing. */ /* * The Group must CONTAIN the cell's controls, not merely appear in it. Asserting * that ` { const from = text.indexOf(open); if (from === -1) return ''; const to = text.indexOf(close, from); // An unclosed Group would otherwise swallow the rest of the cell, which is how // the first version of this let an EMPTY Group pass with the controls left // outside it. return to === -1 ? '' : text.slice(from, to); }; const inGroup = cell === undefined ? '' : between(cell, ''); const controls = (text) => (text.match(/<(?:API)?Button\b| { /* * An icon-only Button carries no text, so without `aria-label` it reaches assistive tech * as an unnamed button. `IconButton` requires a label by construction and * AddToPlaylistButton supplies one; the plain Button and APIButton used for edit, delete * and the catalog preview did not, and this change rewrote all three rows. * * In the source, for the same reason as the Group guard: these rows are built inside the * module and fetched from the API. */ const openingTag = (text, at) => { let depth = 0; for (let i = at; i < text.length; i++) { const c = text[i]; if (c === '{' || c === '(') depth += 1; else if (c === '}' || c === ')') depth -= 1; else if (c === '>' && depth === 0) return {tag: text.slice(at, i + 1), end: i}; } return {tag: '', end: at}; }; /** Every `