| 1 | // fixtures/indent.test.mjs — the indent vocabulary, under plain Node. |
| 2 | // |
| 3 | // align.test.mjs's counterpart, and the difference between them is the |
| 4 | // point. Alignment's value is a string from a closed set, so its tests |
| 5 | // are about a table being consulted and anything outside it dropped. |
| 6 | // Indent's value is a number, so these are about a CLAMP — a different |
| 7 | // mechanism, with its own ways to be wrong. |
| 8 | // |
| 9 | // The cases worth having are the ones no editor can produce. |
| 10 | // POST /d/{sqid}/updates accepts raw Yjs bytes that never met the |
| 11 | // schema, and core/src/fragment.rs writes its output into the page |
| 12 | // unescaped, so "99", "0" and "3; drop" are all reachable and all have |
| 13 | // to render as something safe. core/tests/fragment.rs holds the same |
| 14 | // three in Rust against the same fixture; both sides check |
| 15 | // independently because a document must not indent one distance on |
| 16 | // screen and another on paper. |
| 17 | |
| 18 | import { test } from 'node:test' |
| 19 | import assert from 'node:assert/strict' |
| 20 | |
| 21 | import { MAX_INDENT, INDENT_STEP_IN, indentClass, normaliseIndent, indentFromClasses } from './indent.mjs' |
| 22 | |
| 23 | test('a real level becomes its class', () => { |
| 24 | assert.equal(indentClass(1), 'docs-indent-1') |
| 25 | assert.equal(indentClass(MAX_INDENT), 'docs-indent-' + MAX_INDENT) |
| 26 | }) |
| 27 | |
| 28 | test('zero, null and absent are all the same answer: no indent', () => { |
| 29 | assert.equal(indentClass(0), null) |
| 30 | assert.equal(indentClass(null), null) |
| 31 | assert.equal(indentClass(undefined), null) |
| 32 | assert.equal(indentClass(''), null) |
| 33 | }) |
| 34 | |
| 35 | test('a run of digits too large for any integer type still clamps', () => { |
| 36 | // The last corner where this and core/src/fragment.rs disagreed. |
| 37 | // Rust parses as u32, which a twenty-digit run overflows; it now |
| 38 | // treats an overflow as "too large" and clamps, matching what |
| 39 | // Number() does here. Before that, the same document indented at 8 in |
| 40 | // the editor and rendered flush left for a reader with no |
| 41 | // JavaScript. |
| 42 | assert.equal(normaliseIndent('99999999999999999999'), MAX_INDENT) |
| 43 | assert.equal(indentClass('99999999999999999999'), 'docs-indent-' + MAX_INDENT) |
| 44 | }) |
| 45 | |
| 46 | test('a level past the maximum clamps rather than being emitted', () => { |
| 47 | assert.equal(normaliseIndent(99), MAX_INDENT) |
| 48 | assert.equal(indentClass(99), 'docs-indent-' + MAX_INDENT) |
| 49 | // The failure this guards against is docs-indent-99 reaching a class |
| 50 | // attribute — the value would have come from the document. |
| 51 | assert.equal(indentClass(99).includes('99'), false) |
| 52 | }) |
| 53 | |
| 54 | test('anything that is not a whole number renders flush left', () => { |
| 55 | for (const bad of ['3; drop', 'x', '1.5', -2, NaN, {}, [], '<script>']) { |
| 56 | assert.equal(indentClass(bad), null, `indentClass(${JSON.stringify(bad)}) should be null`) |
| 57 | } |
| 58 | }) |
| 59 | |
| 60 | test('a numeric string is a number, because that is what the wire carries', () => { |
| 61 | // y-prosemirror stores node attributes as Yjs XML attributes, which |
| 62 | // are strings — so the renderer and this module both meet "3" rather |
| 63 | // than 3, and treating only the number type as valid would drop every |
| 64 | // indent in every real document. |
| 65 | assert.equal(normaliseIndent('3'), 3) |
| 66 | assert.equal(indentClass('3'), 'docs-indent-3') |
| 67 | }) |
| 68 | |
| 69 | test('the class round-trips back to its level for a paste', () => { |
| 70 | assert.equal(indentFromClasses(['docs-indent-4']), 4) |
| 71 | assert.equal(indentFromClasses(['docs-align-center', 'docs-indent-2']), 2) |
| 72 | assert.equal(indentFromClasses(['docs-align-center']), null) |
| 73 | assert.equal(indentFromClasses([]), null) |
| 74 | // A class naming a level out of range is not a level this build |
| 75 | // wrote, and must not become one on the way in either. |
| 76 | assert.equal(indentFromClasses(['docs-indent-99']), MAX_INDENT) |
| 77 | assert.equal(indentFromClasses(['docs-indent-0']), null) |
| 78 | }) |
| 79 | |
| 80 | test('the step is the half inch every word processor uses', () => { |
| 81 | // Stated here as a literal rather than derived from the constant, so |
| 82 | // that changing the constant fails this test and makes someone look |
| 83 | // at views/show.html's .docs-indent-N rules and |
| 84 | // internal/export/pdf.go's IndentStepPt, which restate the same |
| 85 | // number and would otherwise silently disagree. |
| 86 | assert.equal(INDENT_STEP_IN, 0.5) |
| 87 | assert.equal(MAX_INDENT, 8) |
| 88 | }) |
| 89 | |