import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { describe, it } from 'node:test' // jsdom has no layout engine, so the mobile row contract is read from the stylesheet just like the // drawer and touch-floor contracts beside this test. const css = readFileSync(new URL('../src/app.css', import.meta.url), 'utf8') const mobile = /@media \(max-width: 560px\) \{([\s\S]*?)\n\}/.exec(css)?.[1] const goalRow = readFileSync(new URL('../src/lib/components/GoalRow.svelte', import.meta.url), 'utf8') const unitRow = readFileSync(new URL('../src/lib/components/UnitRow.svelte', import.meta.url), 'utf8') // The Userinput section draws a foreign board's feedback with the unit row's own furniture, so it // inherits this contract too — and it is a route rather than a component, which is exactly the kind // of row that drifts out of it unwatched. const feedbackRow = readFileSync( new URL('../src/routes/p/[project]/userinput/+page.svelte', import.meta.url), 'utf8', ) describe('rows below 560px', () => { it('floats the goal tail into a name that wraps as text', () => { assert.ok(mobile, 'the 560px block is gone') assert.match(mobile, /\.goal-row \{[^}]*display: flow-root;/) assert.match(mobile, /\.goal-row \.gtitle \{[^}]*display: inline;[^}]*white-space: normal;[^}]*overflow: visible;/) assert.match(mobile, /\.goal-row \.tail \{[^}]*float: right;[^}]*max-width: \d+%;/) assert.doesNotMatch(mobile, /\.goal-row \.who\s*\{[^}]*display:\s*none/) assert.doesNotMatch(mobile, /\.goal-row \.tail \.badge ~ \.badge\s*\{[^}]*display:\s*none/) }) it('floats the unit tail into a summary that wraps as text', () => { assert.ok(mobile, 'the 560px block is gone') assert.match(mobile, /\.row \{[^}]*display: flow-root;/) assert.match(mobile, /\.row \.sub \{[^}]*display: inline;[^}]*white-space: normal;[^}]*overflow: visible;/) assert.match(mobile, /\.row \.tail \{[^}]*float: right;[^}]*max-width: \d+%;/) assert.doesNotMatch(mobile, /\.row(?:\.sysrow)? \.sub\s*\{[^}]*display:\s*none/) assert.doesNotMatch(mobile, /\.row\.sysrow (?:\.when|\.tail > \.disc)\s*\{[^}]*display:\s*none/) }) // A leading indicator that stays in flow is a whole extra line per row, and the gutter reserved // for it sits empty beside it. The unit row leads with a `.dot`; the feedback row leads with a // `.disc`, which is `display: grid` and would take that line. Both have to be named. it('pulls whatever a row leads with into the gutter reserved for it', () => { assert.ok(mobile, 'the 560px block is gone') // `.goal-row > …` contains `.row > …`, so the selector has to be read off a word boundary. const gutter = /(?:^|\s)\.row > ([^{]*)\{[^}]*position: absolute;/m.exec(mobile)?.[1] ?? '' assert.match(gutter, /\.dot\b/, 'the unit row’s state dot stays in flow') assert.match(gutter, /\.disc\b/, 'the feedback row’s author disc stays in flow') assert.match(feedbackRow, / { for (const sentence of ['with an agent', 'open', 'to review']) { assert.match( goalRow, new RegExp(`text: \`[^\`]*${sentence}\`, narrow:`), `${sentence} has no narrow form`, ) } }) // `display: none` takes the sentence out of the accessible tree as well as off the screen, and the // narrow form beside it is aria-hidden — shedding both leaves the badge silent. it('keeps the sentence a badge sheds in the accessible name', () => { assert.ok(mobile, 'the 560px block is gone') assert.doesNotMatch(mobile, /\.badge \.wide \{[^}]*display:\s*none/) assert.match(mobile, /\.badge \.wide \{[^}]*position: absolute;[^}]*clip-path: inset\(50%\);/) }) // A badge left with nothing but its glyph keeps the padding that was making room for a word, and // the pill sits visibly off-centre around it. it('closes a bare badge up around its glyph', () => { assert.ok(mobile, 'the 560px block is gone') assert.match(mobile, /\.badge\.bare \{[^}]*padding-inline:/) }) // A right float only shortens the line boxes that come after it, so the tail has to be written // before the text it floats into. `order` is what puts it back on the end of the desktop flex // line — lose either half and the mobile row silently goes back to costing a whole extra line. it('writes each tail before the text it floats into, and orders it back on the wide layout', () => { assert.ok(goalRow.indexOf('class="tail"') < goalRow.indexOf('class="gtitle"'), 'goal tail moved') assert.ok(unitRow.indexOf('class="tail"') < unitRow.indexOf('class="sub"'), 'unit tail moved') assert.ok( feedbackRow.indexOf('class="tail"') < feedbackRow.indexOf('class="sub titled"'), 'feedback tail moved', ) assert.match(css, /\.goal-row \.tail \{[^}]*order: 1;/) assert.match(css, /\.row \.tail \{[^}]*order: 1;/) }) it('lets admin descriptions and actions wrap', () => { assert.ok(mobile, 'the 560px block is gone') assert.match(mobile, /\.arow\s*\{[^}]*flex-wrap: wrap;/) assert.match(mobile, /\.arow \.meta \{[^}]*white-space: normal;[^}]*overflow-wrap: anywhere;/) assert.match(mobile, /\.arow \.tail \{[^}]*flex-wrap: wrap;/) }) })