// @vitest-environment jsdom import { describe, expect, test } from "vitest"; import { blockStartingWith, build, fixture, outline, paste, pasteFixture, } from "./testHelpers"; // Real clipboard captures live in __fixtures__/captured/; everything else is // hand-written from what we believe a source emits. See README.md. // // Invariants that hold for every fixture regardless of source (no stylesheet // text as content, no literal bullet markers, no collapsed documents) are // asserted once in fixtureInvariants.test.ts. describe("Google Docs — whole document (captured)", () => { const blocks = pasteFixture("captured/google-docs.html"); test("outline", () => { expect(outline(blocks)).toEqual([ "text: Your Company", "text: 123 Your Street", "text: Your City, ST 12345", "text: (123) 456-7890", "text: Project Name", // The superscript "th" merges into the paragraph instead of splitting it; // Leaflet has no superscript mark, so it degrades to plain text. "text: 4th September 20XX", "h1: OVERVIEW", // The source's trailing survives as U+00A0 rather than folding. "text: Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper.\u00A0", "h1: Ordered list", // Docs writes
text
as its own block would double every list item.
"ol-li: Lorem ipsum dolor sit amet, consectetuer adipiscing elit",
"ol-li: Sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.",
"ul-li: Dotted list",
" ul-li: Nest",
" ul-li: Child",
" ul-li: Another",
"h1: SPECIFICATIONS",
"text: Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Typi non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem. Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius.",
"h1: MILESTONES",
"h2: Lorem Ipsum",
"text: Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.",
]);
});
// Docs never emits ; weight is the only bold signal, and body text at
// weight 400 must not pick it up.
test("bold comes from font-weight", () => {
expect(
blocks.filter((b) => b.marks.includes("strong")).map((b) => b.text),
).toEqual([
"Your Company",
"4th September 20XX",
"OVERVIEW",
"Ordered list",
"SPECIFICATIONS",
"MILESTONES",
"Lorem Ipsum",
]);
});
});
// What actually breaks on Google Docs is list nesting: sublists arrive as
// *siblings* of the s tagged `mso-list`, with the bullet as
// literal text; normalizeWordListParagraphs rebuilds the real list.
"ul-li: North region grew",
"ul-li: South region held steady",
" ul-li: Except in March",
"h2: Next steps",
"text: See the plan for details.",
"text: Region | Growth",
"text: North | 12%",
]);
});
test("marks and links", () => {
expect(blockStartingWith(blocks, "Revenue was").marks).toEqual([
"strong",
"em",
"underline",
]);
expect(blockStartingWith(blocks, "See the plan").links).toEqual([
"https://example.com/plan",
]);
});
});
describe("Word Online", () => {
const blocks = pasteFixture("word-online.html");
test("outline", () => {
// One list split across sibling hidden Real text One Two One Two Plain alone rather than swallowing it into the
that follows.
describe("Google Docs — mid-paragraph selection (captured)", () => {
test("outline", () => {
expect(
outline(pasteFixture("captured/google-docs-mid-paragraph.html")),
).toEqual([
// The selection began mid-word, so Docs leads with a .
"text: \u00A0sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper.\u00A0",
"h1: Ordered list",
"ol-li: Lorem ipsum dolor sit amet, consectetuer adipiscing elit",
"ol-li: Sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.",
"ul-li: Dotted list",
" ul-li: Nest",
" ul-li: Child",
" ul-li: Another",
]);
});
});
describe("Notion (captured)", () => {
const blocks = pasteFixture("captured/notion.html");
test("outline", () => {
expect(outline(blocks)).toEqual([
"h1: Weekly To-do List",
"text: Add your weekly to-do’s. You can always add more by typing /to-do in an empty space. Click this button to create a fresh set of to-do’s for a new week. You can drag any old to-do’s from last week to the new week @sdfsdfsdfsdfsdfsdfsdfsdfsd . ↓ asdfasdfas df asdf asdfa sdfas df asdf @",
"text: Multiple paragraphs",
"h2: A heading",
"text: @Jared Pereira no icon ha this is a new subpage",
"blockquote: This is a quote",
"ul-li: A list",
// Notion signs its payload with a trailing ``, which
// must not become a block of its own.
" ul-li: Nested list",
]);
});
test("inline marks stay scoped to their own run", () => {
expect(blockStartingWith(blocks, "Add your weekly").marks).toEqual([
"code",
]);
const link = blockStartingWith(blocks, "@Jared");
expect(link.marks).toEqual(["link"]);
expect(link.links).toEqual([
"https://app.notion.com/p/this-is-a-new-subpage-2bd1f91923908002aa72f31fd6639fa4?pvs=21",
]);
});
});
// An inline element wrapping block-level content. Google Docs was documented to
// wrap whole documents this way; neither capture above does, but Docs still
// does it around images and other editors emit the shape too. Without
// descending into the wrapper the entire document collapses into one block.
describe("inline wrapper around block content", () => {
const blocks = pasteFixture("legacy-docs-inline-wrapper.html");
test("outline", () => {
expect(outline(blocks)).toEqual([
"h1: Trip notes",
"text: We left at 6am, which was much too early. See the itinerary for details.",
"h2: Packing list",
"ul-li: Snacks",
" ul-li: Pretzels",
"ol-li: Leave home",
"ol-li: Drive north",
"text: Cancelled stop and an underlined word.",
"image: ",
"text: Closing thought.",
]);
});
test("marks, links and the wrapped image survive the descent", () => {
const body = blockStartingWith(blocks, "We left at");
// The underline rides along from Docs' own link styling, not from the
// author underlining anything.
expect(body.marks).toEqual(["strong", "em", "underline", "link"]);
expect(body.links).toEqual(["https://example.com/itinerary"]);
expect(blockStartingWith(blocks, "Cancelled stop").marks).toEqual([
"strikethrough",
"underline",
]);
expect(blocks.find((b) => b.type === "image")?.imageURL).toBe(
"https://lh7-us.googleusercontent.com/docsz/photo.png",
);
});
});
describe("Microsoft Word (desktop)", () => {
const blocks = pasteFixture("word-desktop.html");
test("outline", () => {
expect(outline(blocks)).toEqual([
"h1: Quarterly report",
"text: Revenue was up slightly, and margins were flat.",
"text: ",
// Word writes these as flat
s, with depth only in aria-level.
expect(outline(blocks)).toEqual([
"h1: Meeting minutes",
"text: Attendance was strong this quarter.",
"ul-li: Budget approved",
" ul-li: Pending legal review",
]);
});
test("bold comes from the font-weight style", () => {
expect(blockStartingWith(blocks, "Attendance").marks).toEqual(["strong"]);
});
});
describe("Word for Mac", () => {
test("a
`,
["ul-li: Top", " ul-li: Nested", "ul-li: Second top"],
],
[
"a nested list inside its li nests under it",
`
`,
["ul-li: Top", " ul-li: Nested", "ul-li: Second top"],
],
[
"an ordered list nested in an unordered list keeps its own style",
`
`,
["ul-li: Top", " ol-li: One"],
],
[
"a table row becomes one block, not one block per cell",
`
`,
["text: Name | Qty", "text: Bolt | 4"],
],
[
"empty paragraphs are preserved as empty text blocks",
`Name Qty Bolt 4
See this.
`, `this`, ]) { const blocks = paste(html); expect(blocks).toHaveLength(1); expect(blocks[0].type).toBe("text"); expect(blocks[0].links).toEqual(["https://example.com"]); } }); test("a linked image becomes an image block instead of being dropped", () => { const blocks = paste( `Before
`,
);
expect(outline(blocks)).toEqual(["text: Before", "image: "]);
expect(blocks[1].imageURL).toBe("https://example.com/pic.png");
});
});