mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 02:45:38 -06:00
c663c39763
* refactor(test): table-drive tooling test cases * refactor(test): table-drive agent test cases * refactor(test): table-drive core test cases * refactor(test): table-drive plugin test cases * refactor(test): table-drive package test cases * chore(test): prune max-lines baseline * fix(test): preserve table row literal types
423 lines
10 KiB
TypeScript
423 lines
10 KiB
TypeScript
/**
|
|
* Nested List Rendering Tests
|
|
*
|
|
* This test file documents and validates the expected behavior for nested lists
|
|
* when rendering Markdown to plain text.
|
|
*
|
|
* ## Expected Plain Text Behavior
|
|
*
|
|
* Per CommonMark spec, nested lists create a hierarchical structure. When rendering
|
|
* to plain text for messaging platforms, we expect:
|
|
*
|
|
* 1. **Indentation**: Each nesting level adds 2 spaces of indentation
|
|
* 2. **Bullet markers**: Bullet lists use "•" (Unicode bullet)
|
|
* 3. **Ordered markers**: Ordered lists use "N. " format
|
|
* 4. **Line endings**: Each list item ends with a single newline
|
|
* 5. **List termination**: A trailing newline after the entire list (for top-level only)
|
|
*
|
|
* ## markdown-it Token Sequence
|
|
*
|
|
* For nested lists, markdown-it emits tokens in this order:
|
|
* - bullet_list_open (outer)
|
|
* - list_item_open
|
|
* - paragraph_open (hidden=true for tight lists)
|
|
* - inline (with text children)
|
|
* - paragraph_close
|
|
* - bullet_list_open (nested)
|
|
* - list_item_open
|
|
* - paragraph_open
|
|
* - inline
|
|
* - paragraph_close
|
|
* - list_item_close
|
|
* - bullet_list_close
|
|
* - list_item_close
|
|
* - bullet_list_close
|
|
*
|
|
* The key insight is that nested lists appear INSIDE the parent list_item,
|
|
* between the paragraph and the list_item_close.
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { markdownToIR } from "./ir.js";
|
|
|
|
describe("Nested Lists - 2 Level Nesting", () => {
|
|
it("records parser-owned item spans and list ancestry", () => {
|
|
const result = markdownToIR("- parent\n - child\n- next\n# Heading");
|
|
const items = [...(result.listItems ?? [])].toSorted(
|
|
(left, right) => (left.listMarker?.start ?? 0) - (right.listMarker?.start ?? 0),
|
|
);
|
|
const [parent, child, next] = items;
|
|
expect(items).toHaveLength(3);
|
|
expect(parent).toMatchObject({ depth: 0, start: 0 });
|
|
expect(child).toMatchObject({ depth: 1, parentListId: parent?.listId });
|
|
expect(next?.listId).toBe(parent?.listId);
|
|
expect(result.text.slice(parent?.start, parent?.end)).toContain("child");
|
|
expect(result.text.slice(next?.start, next?.end)).not.toContain("Heading");
|
|
});
|
|
|
|
it("keeps loose continuation paragraphs inside the item span", () => {
|
|
const result = markdownToIR("- first\n\n continuation\n- next");
|
|
const first = result.listItems?.find((item) => item.listMarker?.start === 0);
|
|
expect(result.text.slice(first?.start, first?.end)).toContain("continuation");
|
|
});
|
|
|
|
it("renders bullet items nested inside bullet items with proper indentation", () => {
|
|
const input = `- Item 1
|
|
- Nested 1.1
|
|
- Nested 1.2
|
|
- Item 2`;
|
|
|
|
const result = markdownToIR(input);
|
|
|
|
// Expected output:
|
|
// • Item 1
|
|
// • Nested 1.1
|
|
// • Nested 1.2
|
|
// • Item 2
|
|
// Note: markdownToIR trims trailing whitespace, so no final newline
|
|
const expected = `• Item 1
|
|
• Nested 1.1
|
|
• Nested 1.2
|
|
• Item 2`;
|
|
|
|
expect(result.text).toBe(expected);
|
|
});
|
|
|
|
it("renders ordered items nested inside bullet items", () => {
|
|
const input = `- Bullet item
|
|
1. Ordered sub-item 1
|
|
2. Ordered sub-item 2
|
|
- Another bullet`;
|
|
|
|
const result = markdownToIR(input);
|
|
|
|
// Expected output:
|
|
// • Bullet item
|
|
// 1. Ordered sub-item 1
|
|
// 2. Ordered sub-item 2
|
|
// • Another bullet
|
|
const expected = `• Bullet item
|
|
1. Ordered sub-item 1
|
|
2. Ordered sub-item 2
|
|
• Another bullet`;
|
|
|
|
expect(result.text).toBe(expected);
|
|
});
|
|
|
|
it("renders bullet items nested inside ordered items", () => {
|
|
const input = `1. Ordered 1
|
|
- Bullet sub 1
|
|
- Bullet sub 2
|
|
2. Ordered 2`;
|
|
|
|
const result = markdownToIR(input);
|
|
|
|
// Expected output:
|
|
// 1. Ordered 1
|
|
// • Bullet sub 1
|
|
// • Bullet sub 2
|
|
// 2. Ordered 2
|
|
const expected = `1. Ordered 1
|
|
• Bullet sub 1
|
|
• Bullet sub 2
|
|
2. Ordered 2`;
|
|
|
|
expect(result.text).toBe(expected);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
title: "renders ordered items nested inside ordered items",
|
|
markdown: `1. First
|
|
1. Sub-first
|
|
2. Sub-second
|
|
2. Second`,
|
|
expected: `1. First
|
|
1. Sub-first
|
|
2. Sub-second
|
|
2. Second`,
|
|
},
|
|
{
|
|
title: "renders 4 levels of bullet nesting",
|
|
markdown: `- L1
|
|
- L2
|
|
- L3
|
|
- L4
|
|
- Back`,
|
|
expected: `• L1
|
|
• L2
|
|
• L3
|
|
• L4
|
|
• Back`,
|
|
},
|
|
{
|
|
title: "renders 3 levels with multiple items at each level",
|
|
markdown: `- A1
|
|
- B1
|
|
- C1
|
|
- C2
|
|
- B2
|
|
- A2`,
|
|
expected: `• A1
|
|
• B1
|
|
• C1
|
|
• C2
|
|
• B2
|
|
• A2`,
|
|
},
|
|
{
|
|
title: "renders complex mixed nesting (bullet > ordered > bullet)",
|
|
markdown: `- Bullet 1
|
|
1. Ordered 1.1
|
|
- Deep bullet
|
|
2. Ordered 1.2
|
|
- Bullet 2`,
|
|
expected: `• Bullet 1
|
|
1. Ordered 1.1
|
|
• Deep bullet
|
|
2. Ordered 1.2
|
|
• Bullet 2`,
|
|
},
|
|
{
|
|
title: "renders ordered > bullet > ordered nesting",
|
|
markdown: `1. First
|
|
- Sub bullet
|
|
1. Deep ordered
|
|
- Another bullet
|
|
2. Second`,
|
|
expected: `1. First
|
|
• Sub bullet
|
|
1. Deep ordered
|
|
• Another bullet
|
|
2. Second`,
|
|
},
|
|
{
|
|
title: "handles sibling nested lists at same level",
|
|
markdown: `- A
|
|
- A1
|
|
- B
|
|
- B1`,
|
|
expected: `• A
|
|
• A1
|
|
• B
|
|
• B1`,
|
|
},
|
|
])("$title", ({ markdown, expected }) => {
|
|
const input = markdown;
|
|
|
|
const result = markdownToIR(input);
|
|
|
|
expect(result.text).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("Nested Lists - 3+ Level Deep Nesting", () => {
|
|
it("renders 3 levels of bullet nesting", () => {
|
|
const input = `- Level 1
|
|
- Level 2
|
|
- Level 3
|
|
- Back to 1`;
|
|
|
|
const result = markdownToIR(input);
|
|
|
|
// Expected output with progressive indentation:
|
|
// • Level 1
|
|
// • Level 2
|
|
// • Level 3
|
|
// • Back to 1
|
|
const expected = `• Level 1
|
|
• Level 2
|
|
• Level 3
|
|
• Back to 1`;
|
|
|
|
expect(result.text).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("Nested Lists - Mixed Nesting", () => {});
|
|
|
|
describe("Nested Lists - Newline Handling", () => {
|
|
it("does not produce triple newlines in nested lists", () => {
|
|
const input = `- Item 1
|
|
- Nested
|
|
- Item 2`;
|
|
|
|
const result = markdownToIR(input);
|
|
expect(result.text).not.toContain("\n\n\n");
|
|
});
|
|
|
|
it("does not produce double newlines between nested items", () => {
|
|
const input = `- A
|
|
- B
|
|
- C
|
|
- D`;
|
|
|
|
const result = markdownToIR(input);
|
|
|
|
// Between B and C there should be exactly one newline
|
|
expect(result.text).toContain(" • B\n • C");
|
|
expect(result.text).not.toContain(" • B\n\n • C");
|
|
});
|
|
|
|
it("properly terminates top-level list (trimmed output)", () => {
|
|
const input = `- Item 1
|
|
- Nested
|
|
- Item 2`;
|
|
|
|
const result = markdownToIR(input);
|
|
|
|
// markdownToIR trims trailing whitespace, so output should end with Item 2
|
|
// (no trailing newline after trimming)
|
|
expect(result.text).toMatch(/Item 2$/);
|
|
// Should not have excessive newlines before Item 2
|
|
expect(result.text).not.toContain("\n\n• Item 2");
|
|
});
|
|
});
|
|
|
|
describe("Nested Lists - Edge Cases", () => {
|
|
it("handles empty parent with nested items", () => {
|
|
// This is a bit of an edge case - a list item that's just a marker followed by nested content
|
|
const input = `-
|
|
- Nested only
|
|
- Normal`;
|
|
|
|
const result = markdownToIR(input);
|
|
|
|
// Should still render the nested item with proper indentation
|
|
expect(result.text).toContain(" • Nested only");
|
|
});
|
|
|
|
it("handles nested list as first child of parent item", () => {
|
|
const input = `- Parent text
|
|
- Child
|
|
- Another parent`;
|
|
|
|
const result = markdownToIR(input);
|
|
|
|
// The child should appear indented under the parent
|
|
expect(result.text).toContain("• Parent text\n • Child");
|
|
});
|
|
});
|
|
|
|
describe("list paragraph spacing", () => {
|
|
it.each([
|
|
{
|
|
title: "preserves paragraph breaks inside loose bullet list items",
|
|
markdown: `- first paragraph
|
|
|
|
second paragraph
|
|
- next`,
|
|
expected: `• first paragraph
|
|
|
|
second paragraph
|
|
|
|
• next`,
|
|
},
|
|
{
|
|
title: "preserves paragraph breaks inside loose ordered list items",
|
|
markdown: `1. first paragraph
|
|
|
|
second paragraph
|
|
2. next`,
|
|
expected: `1. first paragraph
|
|
|
|
second paragraph
|
|
|
|
2. next`,
|
|
},
|
|
{
|
|
title: "preserves paragraph breaks inside loose blockquoted list items",
|
|
markdown: `> - first paragraph
|
|
>
|
|
> second paragraph
|
|
> - next`,
|
|
expected: `• first paragraph
|
|
|
|
second paragraph
|
|
|
|
• next`,
|
|
},
|
|
{
|
|
title: "keeps tight heading list items single-spaced",
|
|
markdown: `- # A
|
|
- # B`,
|
|
expected: `• A
|
|
• B`,
|
|
},
|
|
{
|
|
title: "keeps tight blockquote list items single-spaced",
|
|
markdown: `- > quote
|
|
- next`,
|
|
expected: `• quote
|
|
• next`,
|
|
},
|
|
])("$title", ({ markdown, expected }) => {
|
|
const input = markdown;
|
|
|
|
const result = markdownToIR(input);
|
|
|
|
expect(result.text).toBe(expected);
|
|
});
|
|
|
|
it("does not add triple newlines before loose nested bullet lists", () => {
|
|
const input = `- parent
|
|
|
|
- child
|
|
|
|
- next`;
|
|
|
|
const result = markdownToIR(input);
|
|
|
|
expect(result.text).toBe(`• parent
|
|
|
|
• child
|
|
• next`);
|
|
expect(result.text).not.toContain("\n\n\n");
|
|
});
|
|
|
|
it("does not add triple newlines before loose nested ordered lists", () => {
|
|
const input = `1. parent
|
|
|
|
1. child
|
|
|
|
2. next`;
|
|
|
|
const result = markdownToIR(input);
|
|
|
|
expect(result.text).toBe(`1. parent
|
|
|
|
1. child
|
|
2. next`);
|
|
expect(result.text).not.toContain("\n\n\n");
|
|
});
|
|
|
|
it("adds blank line between bullet list and following paragraph", () => {
|
|
const input = `- item 1
|
|
- item 2
|
|
|
|
Paragraph after`;
|
|
const result = markdownToIR(input);
|
|
// Should have two newlines between "item 2" and "Paragraph"
|
|
expect(result.text).toContain("item 2\n\nParagraph");
|
|
});
|
|
|
|
it("adds blank line between ordered list and following paragraph", () => {
|
|
const input = `1. item 1
|
|
2. item 2
|
|
|
|
Paragraph after`;
|
|
const result = markdownToIR(input);
|
|
expect(result.text).toContain("item 2\n\nParagraph");
|
|
});
|
|
|
|
it("does not produce triple newlines", () => {
|
|
const input = `- item 1
|
|
- item 2
|
|
|
|
Paragraph after`;
|
|
const result = markdownToIR(input);
|
|
// Should NOT have three consecutive newlines
|
|
expect(result.text).not.toContain("\n\n\n");
|
|
});
|
|
});
|