/// The native element: syntax highlighting, line numbers, selection. import fs from "fs" import path from "path" import React from "react" import { beforeAll, describe, expect, it } from "vitest" import { createTestRoot } from "../testing.js" import { expectScreenshotsDiffer, SHOTS_DIR } from "./test-utils.js" const TS_SOURCE = `interface User { id: number name: string } export function greet(user: User): string { // Say hello. return \`hello \${user.name}\` }` beforeAll(() => { fs.mkdirSync(SHOTS_DIR, { recursive: true }) }) /** Default `codeLineHeight`. One row per source line, at this exact height. */ const LINE_HEIGHT = 18 function codeBounds(renderer: { findByType(type: string): { id: number }[]; getElementBounds(id: number): { x: number; y: number; width: number; height: number } | null }) { const node = renderer.findByType("code")[0] expect(node).toBeDefined() const bounds = renderer.getElementBounds(node!.id) expect(bounds).not.toBeNull() return bounds! } describe("", () => { it("renders one row per source line", () => { const { render, renderer } = createTestRoot() render() // Only the code paints. No language header, no chrome of any kind. expect(renderer.getPaintedText()).toEqual(["a", "b", "c"]) }) it("normalizes CRLF source lines", () => { const { render, renderer } = createTestRoot() render(
) expect(renderer.getPaintedText()).toEqual(["// one", "// two", ""]) expect(renderer.dragSelect(22, 25, 900, 500)).toBe("// one\n// two") }) it("paints no surface of its own", () => { const { render, renderer } = createTestRoot() render() // Exactly the rows: no padding, no header strip, no border. expect(codeBounds(renderer).height).toBe(3 * LINE_HEIGHT) }) it("keeps JSON-looking source strings as source text", () => { const cases = ["true", "null", '{"a":1}', "ordinary text"] for (const code of cases) { const { render, renderer } = createTestRoot() render() expect(renderer.getPaintedText()).toContain(code) } }) it("renders an empty code block without crashing", () => { const { render, renderer } = createTestRoot() render() expect(renderer.findByType("code")).toHaveLength(1) }) it("never paints the language as a header", () => { const { render, renderer } = createTestRoot() render() expect(renderer.getPaintedText()).not.toContain("python") }) it("grows by the padding from the style prop", () => { const { render, renderer } = createTestRoot() render() expect(codeBounds(renderer).height).toBe(3 * LINE_HEIGHT + 40) }) it("takes the line height and font size from the style prop", () => { const { render, renderer } = createTestRoot() render() // The row height follows style.lineHeight, so tall glyphs are never clipped. expect(codeBounds(renderer).height).toBe(3 * 30) }) it("scales the rows when only fontSize is given", () => { const { render, renderer } = createTestRoot() // Double the glyphs and the rows must double too, or the lines overlap. render() expect(codeBounds(renderer).height).toBe(3 * 2 * LINE_HEIGHT) }) it("paints the fill from the style prop", () => { const bare = path.join(SHOTS_DIR, "code-style-bare.png") const filled = path.join(SHOTS_DIR, "code-style-filled.png") const a = createTestRoot() a.render(
) a.renderer.captureScreenshot(bare) const b = createTestRoot() b.render(
) b.renderer.captureScreenshot(filled) expectScreenshotsDiffer(bare, filled) }) it("renders line numbers when asked", () => { const { render, renderer } = createTestRoot() render() // Gutter numbers paint before their line, so the log interleaves them. expect(renderer.getPaintedText()).toEqual(["1", "a", "2", "b", "3", "c"]) }) it("keeps code text selectable", () => { const { render, renderer } = createTestRoot() render(
) const selected = renderer.dragSelect(22, 25, 900, 42) expect(selected).toBe("const answer = 42") }) it("selects across several code lines", () => { const { render, renderer } = createTestRoot() render(
) const selected = renderer.dragSelect(22, 25, 900, 500) expect(selected).toBe("one\ntwo\nthree") }) it("does not select the line-number gutter", () => { const { render, renderer } = createTestRoot() render(
) // Anchor inside the code column, past the gutter, and drag to the end. const selected = renderer.dragSelect(70, 25, 900, 500) // The gutter painted this frame, but a drag must never pick it up: the // exact anchor column is font-dependent, the absence of digits is not. expect(renderer.getPaintedText()).toContain("1") expect(selected).not.toMatch(/\d/) expect(selected?.endsWith("beta")).toBe(true) }) it("starts a selection in the gutter and still skips line numbers", () => { const { render, renderer } = createTestRoot() render(
) const selected = renderer.dragSelect(24, 25, 900, 500) expect(selected).toBe("alpha\nbeta") }) it("changes appearance when a syntax theme is applied", () => { const before = path.join(SHOTS_DIR, "code-theme-default.png") const after = path.join(SHOTS_DIR, "code-theme-custom.png") const a = createTestRoot() a.render(
) a.renderer.captureScreenshot(before) const b = createTestRoot() b.render(
) b.renderer.captureScreenshot(after) expectScreenshotsDiffer(before, after) }) it("captures a reference screenshot of a highlighted block", () => { const shot = path.join(SHOTS_DIR, "code-typescript.png") const { render, renderer } = createTestRoot() render(
{/* The card is the caller's, built from `style` alone. */}
) renderer.captureScreenshot(shot) expect(fs.existsSync(shot)).toBe(true) expect(fs.statSync(shot).size).toBeGreaterThan(0) }) it("lets a parent scroller take a vertical wheel over a wide block", () => { const { render, renderer } = createTestRoot() render(
below
) const container = renderer .findByType("div") .find((d) => d.style.overflowY === "scroll") expect(container).toBeDefined() expect(renderer.getScrollOffset(container!.id)).toEqual([0, 0]) renderer.nativeSimulateScrollWheel(80, 50, 0, -80) const offset = renderer.getScrollOffset(container!.id) expect(offset).not.toBeNull() expect(offset![1]).toBeLessThan(0) }) })