taskListRowLayout.test.ts3.4 KBView on GitHub /**
* The checkbox and its text must sit on ONE row.
*
* TipTap renders a task item as `<li><label><input></label><div><p>text</p></div></li>`. `label`
* is inline and `div` is block, so with no layout rule the text starts on the line BELOW the
* checkbox — reported as "the text renders significantly lower than the checkbox". Only the input
* was styled; the row was never laid out.
*
* jsdom does no layout, so this cannot assert pixels. It asserts the two things that were actually
* wrong and that a future edit could quietly break: the DOM shape the CSS selectors depend on, and
* the presence of the rules that lay it out. If TipTap changes `li > div` to something else, the
* first half fails; if someone deletes the flex rule, the second does.
*/
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { Editor } from '@tiptap/core';
import { Markdown } from '@tiptap/markdown';
import StarterKit from '@tiptap/starter-kit';
import TaskList from '@tiptap/extension-task-list';
import TaskItem from '@tiptap/extension-task-item';
const GLOBALS = readFileSync(join(process.cwd(), 'app/globals.css'), 'utf8');
function renderTaskList(markdown: string): string {
const editor = new Editor({
extensions: [StarterKit, Markdown, TaskList, TaskItem.configure({ nested: true })],
content: markdown,
// Same handshake the coaching-fence test uses — without it the Markdown extension never
// parses and the checklist arrives as a literal paragraph.
contentType: 'markdown' as never,
});
const html = editor.getHTML();
editor.destroy();
return html;
}
describe('task-list rows', () => {
it('renders the label and the content as siblings inside the li — the shape the CSS targets', () => {
const html = renderTaskList('- [ ] Asked "what piqued your interest"\n');
expect(html).toContain('data-type="taskList"');
expect(html).toContain('data-type="taskItem"');
// The two children the layout rules select. Both must exist, or `li > label` / `li > div`
// select nothing and the row silently goes back to stacking.
expect(html).toMatch(/<li[^>]*>\s*<label>/);
expect(html).toMatch(/<\/label>\s*<div>/);
});
it('lays the row out as a flex line, not two stacked blocks', () => {
// The bug in one assertion: without this rule the div wraps below the label.
expect(GLOBALS).toMatch(/ul\[data-type='taskList'\] li \{[^}]*display:\s*flex/);
expect(GLOBALS).toMatch(/ul\[data-type='taskList'\] li \{[^}]*align-items:\s*flex-start/);
});
it('gives the label a fixed width and the content the remainder', () => {
expect(GLOBALS).toMatch(/ul\[data-type='taskList'\] li > label \{[^}]*flex:\s*0 0 auto/);
expect(GLOBALS).toMatch(/ul\[data-type='taskList'\] li > div \{[^}]*flex:\s*1 1 auto/);
// Without min-width:0 a long unbroken token pushes the checkbox off the row.
expect(GLOBALS).toMatch(/ul\[data-type='taskList'\] li > div \{[^}]*min-width:\s*0/);
});
it('keeps the 1px nudge on the markdown path only, where there is no flex label', () => {
// remarkGfm (chat, draft bodies, roadmap) emits the input inline with no wrapper, so it still
// needs the baseline nudge. Applying it to the TipTap box too would double-count.
expect(GLOBALS).toMatch(
/li\.task-list-item input\[type='checkbox'\],\s*\n\s*ul\.contains-task-list input\[type='checkbox'\] \{\s*\n\s*margin-top: 1px;/,
);
});
});