/**
* What the runs become in the DOM — the half `inlineMarkdown.test.ts`
* deliberately does not assert.
*
* The load-bearing test here is the nesting one. Every other
* assertion in this file would still pass if the component wrapped
* itself in an anchor, the page would look exactly right, and half
* of each title would quietly stop opening the task.
*
* Subjects are the composed **stories**, per this package's
* story/test split — so what is asserted is what a reader sees.
*/
import { composeStories } from "@storybook/react"
import { expect, userEvent } from "storybook/test"
import { test } from "vitest"
import { mountStory } from "../mountStory.testHelpers.ts"
import { expectAgentDrivable } from "../testing/index.ts"
import * as stories from "./MarkdownLine.stories.tsx"
const {
AllMarks,
AsALink,
Playground,
FileNames,
InsideACardLink,
RefusedUrl,
Routed,
TitleAsALink,
} = composeStories(stories)
test("a code span is a `code` element, and the backticks are gone", async () => {
const { canvasElement } = await mountStory(Playground)
await expect(
[...canvasElement.querySelectorAll("code")].map(
(element) => element.textContent,
),
).toEqual(["Downloads/MOVIES", String.raw`G:\Movies`])
await expect(canvasElement.textContent).toBe(
String.raw`Ingest 53 movies from Downloads/MOVIES into G:\Movies`,
)
})
test("each mark renders its own element", async () => {
const { canvasElement } = await mountStory(AllMarks)
await expect(
canvasElement.querySelector("strong")?.textContent,
).toBe("Urgent:")
await expect(
canvasElement.querySelector("em")?.textContent,
).toBe("Halloween")
await expect(
canvasElement.querySelector("s")?.textContent,
).toBe("Buy a second spool")
})
/**
* The rule that keeps a file name a file name. Asserted on the DOM
* rather than only on the runs because this is the one a reader
* reports — *"why is half my path in italics"* — and the story is
* what they would be looking at.
*/
test("an underscore inside a word marks nothing", async () => {
const { canvasElement } = await mountStory(FileNames)
await expect(canvasElement.querySelector("em")).toBe(null)
await expect(canvasElement.querySelector("strong")).toBe(
null,
)
await expect(canvasElement.textContent).toContain(
"ingest_the_files.sh",
)
})
test("a line with no href has nothing to click", async () => {
const { canvasElement } = await mountStory(Playground)
await expect(canvasElement.querySelector("a")).toBe(null)
})
/**
* `` is not something React refuses and not something the
* browser reports. The HTML parser closes the outer anchor at the
* inner one's start tag, so the text *after* the inner link stops
* being part of the card's link — it looks correct and half of it
* does not work.
*/
test("a link inside the line is a SIBLING anchor, never a nested one", async () => {
const { canvas, canvasElement } =
await mountStory(AsALink)
await expect(canvasElement.querySelector("a a")).toBe(
null,
)
await expect(
canvas
.getAllByRole("link")
.map((link: HTMLElement) =>
link.getAttribute("href"),
),
).toEqual([
"/tasks/7",
"/tasks/7",
"https://example.invalid/pulls/53",
"/tasks/7",
"https://example.invalid/pulls/53",
])
})
/**
* Each anchor is named by the words it covers, and the names are
* therefore DISTINCT.
*
* Naming all three after the whole line was tried first and is
* worse: two links with one name are ambiguous to anything driving
* the page by name, and `expectAgentDrivable` refuses it outright.
* What is announced now matches what is on screen, which is also
* WCAG 2.5.3.
*/
test("split anchors are named by their own words", async () => {
const { canvas } = await mountStory(Routed)
const link = expectAgentDrivable(canvas, {
name: "Ship",
role: "link",
})
await expect(link).toHaveAttribute("href", "/tasks/7")
await expect(
expectAgentDrivable(canvas, {
name: "tonight",
role: "link",
}),
).toHaveAttribute("href", "/tasks/7")
})
test("Tab reaches every anchor in the line", async () => {
const { canvas } = await mountStory(Routed)
const links = canvas.getAllByRole("link")
await userEvent.tab()
await expect(links[0]).toHaveFocus()
await userEvent.tab()
await expect(links[1]).toHaveFocus()
})
/**
* The injected router gets the in-app destination and never gets the
* off-origin one, which would push a route the SPA does not have
* onto its history stack.
*/
test("the router is handed the in-app destination only", async () => {
const { canvas } = await mountStory(Routed)
const [task, pullRequest] = canvas.getAllByRole("link")
await expect(task).toHaveAttribute("data-router", "soft")
await expect(pullRequest).not.toHaveAttribute(
"data-router",
)
await expect(pullRequest).toHaveAttribute(
"target",
"_blank",
)
await expect(pullRequest).toHaveAttribute(
"rel",
"noopener noreferrer",
)
})
/**
* The guard itself lives in `safeUrls.ts` and is tested there. What
* matters here is the *response* to a refusal: no anchor at all,
* rather than a dead one.
*/
test("a refused scheme renders its source as text", async () => {
const { canvasElement } = await mountStory(RefusedUrl)
await expect(canvasElement.querySelector("a")).toBe(null)
await expect(canvasElement.textContent).toBe(
"[click me](javascript:alert(1))",
)
})
/**
* THE SPACES AROUND A MARK SURVIVE INTO THE ACCESSIBLE NAME.
*
* The name computation trims each **element** child's contribution
* before joining them, so a line whose every run was wrapped in a
* `` announced as `Ingest 53 movies fromDownloads/MOVIESinto`
* — painted correctly, announced as one run-on word, and invisible
* to any assertion that reads `textContent`.
*
* Found through `Board`'s move handle, which is named after the
* card's title. The fix is that an unmarked run is a text node
* rather than a wrapped one; this is the guard that keeps it.
*/
test("the accessible name keeps the spaces around a mark", async () => {
const { canvas } = await mountStory(TitleAsALink)
await expect(
canvas.getByRole("link", {
name: "Ingest 53 movies from Downloads/MOVIES tonight",
}),
).toBeInTheDocument()
})
/**
* `isInsideLink` emits no anchor of its own — not even for a bare
* URL, which is the case that makes it a prop rather than a
* convention. A captured note ending in a URL would otherwise nest
* an anchor inside the card's link with nobody having typed `[]()`.
*/
test("isInsideLink draws the marks and adds no anchor", async () => {
const { canvas, canvasElement } =
await mountStory(InsideACardLink)
await expect(canvasElement.querySelector("a a")).toBe(
null,
)
await expect(canvas.getAllByRole("link")).toHaveLength(1)
await expect(
canvasElement.querySelector("code")?.textContent,
).toBe("Downloads/MOVIES")
})