|
| 1 | +// @vitest-environment happy-dom |
| 2 | + |
| 3 | +import { beforeEach, describe, expect, it } from "vitest"; |
| 4 | +import { |
| 5 | + findTitleElement, |
| 6 | + insertButtonContainer, |
| 7 | + resolveInsertionAnchor, |
| 8 | +} from "./dom"; |
| 9 | + |
| 10 | +const CONTAINER_ATTR = "data-copy-markdown-button"; |
| 11 | + |
| 12 | +function makeContainer(): HTMLElement { |
| 13 | + const container = document.createElement("div"); |
| 14 | + container.setAttribute(CONTAINER_ATTR, "true"); |
| 15 | + return container; |
| 16 | +} |
| 17 | + |
| 18 | +/** Mirrors a Docusaurus blog post page: <h1> lives inside a <header> |
| 19 | + * that also holds the date and author (profile) metadata. */ |
| 20 | +function renderBlogPostPage(): void { |
| 21 | + document.body.innerHTML = ` |
| 22 | + <article> |
| 23 | + <header> |
| 24 | + <h1>Logistic regression</h1> |
| 25 | + <div class="blog-date"><time datetime="2025-08-28">2025년 8월 28일</time> · 약 3분</div> |
| 26 | + <div class="blog-authors"><div class="author-name">Eunkwang Shin</div></div> |
| 27 | + </header> |
| 28 | + <div class="markdown"><h2>단변량 선형 회귀</h2><p>body</p></div> |
| 29 | + </article> |
| 30 | + `; |
| 31 | +} |
| 32 | + |
| 33 | +/** Mirrors a Docusaurus docs page: <h1> sits inside .theme-doc-markdown, |
| 34 | + * with no surrounding <header>. */ |
| 35 | +function renderDocsPage(): void { |
| 36 | + document.body.innerHTML = ` |
| 37 | + <article> |
| 38 | + <div class="theme-doc-markdown markdown"> |
| 39 | + <h1>Logistic regression</h1> |
| 40 | + <p>body</p> |
| 41 | + </div> |
| 42 | + </article> |
| 43 | + `; |
| 44 | +} |
| 45 | + |
| 46 | +/** Mirrors a Docusaurus blog list page: post previews use <h2> links |
| 47 | + * (not <h1>), plus a "recent posts" sidebar of <h2> links. */ |
| 48 | +function renderBlogListPage(): void { |
| 49 | + document.body.innerHTML = ` |
| 50 | + <aside><nav><h2>최근 포스트</h2><ul><li><a href="/blog/a">A</a></li></ul></nav></aside> |
| 51 | + <main> |
| 52 | + <article><h2><a href="/blog/a">Post A</a></h2><p>excerpt</p></article> |
| 53 | + <article><h2><a href="/blog/b">Post B</a></h2><p>excerpt</p></article> |
| 54 | + </main> |
| 55 | + `; |
| 56 | +} |
| 57 | + |
| 58 | +beforeEach(() => { |
| 59 | + document.body.innerHTML = ""; |
| 60 | +}); |
| 61 | + |
| 62 | +describe("findTitleElement", () => { |
| 63 | + it("finds the main <h1> on a blog post page", () => { |
| 64 | + renderBlogPostPage(); |
| 65 | + expect(findTitleElement()?.textContent).toBe("Logistic regression"); |
| 66 | + }); |
| 67 | + |
| 68 | + it("finds the main <h1> on a docs page", () => { |
| 69 | + renderDocsPage(); |
| 70 | + expect(findTitleElement()?.textContent).toBe("Logistic regression"); |
| 71 | + }); |
| 72 | + |
| 73 | + it("returns null on a blog list page (previews are <h2>, not <h1>)", () => { |
| 74 | + renderBlogListPage(); |
| 75 | + expect(findTitleElement()).toBeNull(); |
| 76 | + }); |
| 77 | +}); |
| 78 | + |
| 79 | +describe("resolveInsertionAnchor", () => { |
| 80 | + it("returns the enclosing <header> for blog posts", () => { |
| 81 | + renderBlogPostPage(); |
| 82 | + const titleEl = findTitleElement(); |
| 83 | + if (!titleEl) throw new Error("expected a title element"); |
| 84 | + |
| 85 | + const anchor = resolveInsertionAnchor(titleEl, "blog"); |
| 86 | + expect(anchor.tagName).toBe("HEADER"); |
| 87 | + }); |
| 88 | + |
| 89 | + it("returns the <h1> itself for docs", () => { |
| 90 | + renderDocsPage(); |
| 91 | + const titleEl = findTitleElement(); |
| 92 | + if (!titleEl) throw new Error("expected a title element"); |
| 93 | + |
| 94 | + const anchor = resolveInsertionAnchor(titleEl, "docs"); |
| 95 | + expect(anchor).toBe(titleEl); |
| 96 | + }); |
| 97 | + |
| 98 | + it("falls back to the <h1> for blog posts without a <header>", () => { |
| 99 | + document.body.innerHTML = "<article><h1>No header here</h1></article>"; |
| 100 | + const titleEl = findTitleElement(); |
| 101 | + if (!titleEl) throw new Error("expected a title element"); |
| 102 | + |
| 103 | + const anchor = resolveInsertionAnchor(titleEl, "blog"); |
| 104 | + expect(anchor).toBe(titleEl); |
| 105 | + }); |
| 106 | +}); |
| 107 | + |
| 108 | +describe("insertButtonContainer — blog post (포스트 내부)", () => { |
| 109 | + it("inserts the button after the header, below the profile metadata", () => { |
| 110 | + renderBlogPostPage(); |
| 111 | + const titleEl = findTitleElement(); |
| 112 | + if (!titleEl) throw new Error("expected a title element"); |
| 113 | + |
| 114 | + const container = makeContainer(); |
| 115 | + insertButtonContainer(titleEl, "blog", container); |
| 116 | + |
| 117 | + const header = document.querySelector("header"); |
| 118 | + const date = document.querySelector(".blog-date"); |
| 119 | + const author = document.querySelector(".author-name"); |
| 120 | + if (!(header && date && author)) throw new Error("fixture mismatch"); |
| 121 | + |
| 122 | + // The button container is a sibling placed right after the whole header. |
| 123 | + expect(header.nextElementSibling).toBe(container); |
| 124 | + |
| 125 | + // It is NOT wedged between the title and the date/author profile block. |
| 126 | + expect(titleEl.nextElementSibling).not.toBe(container); |
| 127 | + |
| 128 | + // Document order: title -> date -> author -> button. |
| 129 | + const following = Node.DOCUMENT_POSITION_FOLLOWING; |
| 130 | + expect(titleEl.compareDocumentPosition(container) & following).toBeTruthy(); |
| 131 | + expect(date.compareDocumentPosition(container) & following).toBeTruthy(); |
| 132 | + expect(author.compareDocumentPosition(container) & following).toBeTruthy(); |
| 133 | + }); |
| 134 | +}); |
| 135 | + |
| 136 | +describe("insertButtonContainer — docs page", () => { |
| 137 | + it("inserts the button immediately after the <h1>", () => { |
| 138 | + renderDocsPage(); |
| 139 | + const titleEl = findTitleElement(); |
| 140 | + if (!titleEl) throw new Error("expected a title element"); |
| 141 | + |
| 142 | + const container = makeContainer(); |
| 143 | + insertButtonContainer(titleEl, "docs", container); |
| 144 | + |
| 145 | + expect(titleEl.nextElementSibling).toBe(container); |
| 146 | + }); |
| 147 | +}); |
0 commit comments