palabras: blinda el fix CRÍTICO — insertLineBreak code/table anidado

El fix crítico de la auditoría (insertLineBreak enruta code/table a \n a
CUALQUIER profundidad, no solo top-level) iba sin test. Añade 5 casos en
paragraph-ops.test.ts:

- code top-level → \n, sin nodo break (fija la rama).
- code anidado en callout → \n, sin break (DISCRIMINA).
- code anidado en columna → \n, sin break (DISCRIMINA).
- celda de tabla → \n vía Shift+Enter, no solo Enter (fija la rama).
- tabla anidada en callout → \n en la celda (DISCRIMINA).

Discriminación demostrada revirtiendo el fix al check solo-top-level: caen
EXACTAMENTE los 3 casos anidados y los 2 top-level siguen verdes. Con el fix
puesto, 30/30 verde; check con 0 errores en palabras.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alpha-0.1-sec-dom
dev 3 months ago
parent 36a9bf6b09
commit 10118f532a

@ -291,6 +291,98 @@ describe('insertParagraph — code → line break', () => {
});
});
// ── insertLineBreak: code / table route to \n at ANY depth ───────────────
//
// Regression guard for the audit's CRITICAL fix. insertLineBreak (Shift+Enter)
// must send code / table cells to a LITERAL newline — never an atomic break
// inline — at every nesting depth. Before the fix it inspected only the
// top-level block, so code / table nested in a callout or column received a
// break inline, which is illegal in code's text-only children (validate
// rejects it). The nested cases below fail against the pre-fix implementation.
describe('insertLineBreak — code / table → \\n (incl. nested)', () => {
it('top-level code → literal newline, never a break node', () => {
const d = doc({ type: 'code', children: [{ type: 'text', text: 'x' }] });
const sel = createCollapsedSelection([0, 0], 1);
const r = insertLineBreak(createState(d, sel));
expect(r.changed).toBe(true);
const code = r.state.document.children[0];
if (code.type !== 'code') throw new Error('expected code');
expect(code.children.every((c) => c.type === 'text')).toBe(true);
expect(resolveTextNode(r.state.document, [0, 0])!.text).toBe('x\n');
});
it('code nested in a callout → literal newline, never a break node', () => {
const d = doc({
type: 'callout',
intent: 'risk',
children: [{ type: 'code', children: [{ type: 'text', text: 'x' }] }]
});
const sel = createCollapsedSelection([0, 0, 0], 1);
const r = insertLineBreak(createState(d, sel));
expect(r.changed).toBe(true);
const callout = r.state.document.children[0];
if (callout.type !== 'callout') throw new Error('expected callout');
const code = callout.children[0];
if (code.type !== 'code') throw new Error('expected code');
// No break inline smuggled into the code's text-only children.
expect(code.children.every((c) => c.type === 'text')).toBe(true);
expect(resolveTextNode(r.state.document, [0, 0, 0])!.text).toBe('x\n');
});
it('code nested in a column → literal newline, never a break node', () => {
const d = doc({
type: 'columns',
columns: [
{ children: [{ type: 'code', children: [{ type: 'text', text: 'x' }] }] },
{ children: [{ type: 'paragraph', children: [{ type: 'text', text: '' }] }] }
]
});
const sel = createCollapsedSelection([0, 0, 0, 0], 1);
const r = insertLineBreak(createState(d, sel));
expect(r.changed).toBe(true);
expect(resolveTextNode(r.state.document, [0, 0, 0, 0])!.text).toBe('x\n');
});
it('table cell → literal newline (via Shift+Enter, not just Enter)', () => {
const d = doc({
type: 'table',
rows: [
{
cells: [{ children: [{ type: 'paragraph', children: [{ type: 'text', text: 'cell' }] }] }]
}
]
});
const sel = createCollapsedSelection([0, 0, 0, 0, 0], 4);
const r = insertLineBreak(createState(d, sel));
expect(r.changed).toBe(true);
expect(resolveTextNode(r.state.document, [0, 0, 0, 0, 0])!.text).toBe('cell\n');
});
it('table nested in a callout → literal newline in the cell', () => {
const d = doc({
type: 'callout',
intent: 'risk',
children: [
{
type: 'table',
rows: [
{
cells: [
{ children: [{ type: 'paragraph', children: [{ type: 'text', text: 'cell' }] }] }
]
}
]
}
]
});
const sel = createCollapsedSelection([0, 0, 0, 0, 0, 0], 4);
const r = insertLineBreak(createState(d, sel));
expect(r.changed).toBe(true);
expect(resolveTextNode(r.state.document, [0, 0, 0, 0, 0, 0])!.text).toBe('cell\n');
});
});
// ── Atomic blocks: image / divider ───────────────────────────────────────
describe('insertParagraph — atomic blocks', () => {

Loading…
Cancel
Save

Powered by TurnKey Linux.