1import { describe, expect, test } from "bun:test";
2import { annotate, applyEdit, strip } from "../src/source.ts";
3
4const PAGE = `---
5const title = "<not a tag>";
6---
7<section>
8 <h1 data-copy class="hero">Reach for the better primitives.</h1>
9 <p
10 data-copy
11 class="lede"
12 >
13 Already have a Vite app? One resource in{" "}
14 <code>alchemy.run.ts</code> is the whole
15 integration &mdash; really.
16 </p>
17 <p data-copy>{title}</p>
18 <script>const x = "<h2 data-copy>not markup</h2>";</script>
19</section>
20`;
21
22// The DOM text nodes the browser produces for each element.
23const H1 = ["Reach for the better primitives."];
24const LEDE = [
25 "\n Already have a Vite app? One resource in \n ",
26 "alchemy.run.ts",
27 " is the whole\n integration — really.\n ",
28];
29
30describe("annotate", () => {
31 test("stamps each data-copy with its file and index", () => {
32 const out = annotate(PAGE, "src/index.astro");
33 expect(out).toContain(
34 '<h1 data-copy="src/index.astro#0" data-copy-format="inline" class="hero">',
35 );
36 expect(out).toContain('data-copy="src/index.astro#1"');
37 expect(out).toContain('<p data-copy="src/index.astro#2">');
38 });
39
40 test("ignores frontmatter and script contents", () => {
41 const out = annotate(PAGE, "f");
42 expect(out).toContain('"<h2 data-copy>not markup</h2>"');
43 expect(out).not.toContain("#3");
44 });
45});
46
47describe("strip", () => {
48 test("removes every data-copy attribute", () => {
49 const out = strip(PAGE);
50 expect(out).toContain('<h1 class="hero">');
51 expect(out).toContain('<p\n class="lede"\n >');
52 expect(out).toContain("<p>{title}</p>");
53 // Script contents are left alone.
54 expect(out).toContain('"<h2 data-copy>not markup</h2>"');
55 });
56});
57
58describe("applyEdit", () => {
59 test("rewrites a single text run", () => {
60 const out = applyEdit(PAGE, {
61 index: 0,
62 before: H1,
63 after: ["Reach for better primitives."],
64 });
65 expect(out).toContain(
66 '<h1 data-copy class="hero">Reach for better primitives.</h1>',
67 );
68 });
69
70 test("keeps line wrapping, spacers, entities, and tags outside the changed words", () => {
71 const after = [...LEDE];
72 after[2] = " is the entire\n integration — really.\n ";
73 const out = applyEdit(PAGE, { index: 1, before: LEDE, after });
74 expect(out).toContain(
75 ' Already have a Vite app? One resource in{" "}\n <code>alchemy.run.ts</code> is the entire\n integration &mdash; really.',
76 );
77 });
78
79 test("edits text inside nested tags", () => {
80 const after = [...LEDE];
81 after[1] = "alchemy.config.ts";
82 const out = applyEdit(PAGE, { index: 1, before: LEDE, after });
83 expect(out).toContain("<code>alchemy.config.ts</code>");
84 });
85
86 test("escapes characters that are special in templates", () => {
87 const out = applyEdit(PAGE, {
88 index: 0,
89 before: H1,
90 after: ["Use <T> & {x}"],
91 });
92 expect(out).toContain("Use &lt;T&gt; &amp; &#123;x&#125;</h1>");
93 });
94
95 test("deletes and inserts words", () => {
96 const removed = applyEdit(PAGE, {
97 index: 0,
98 before: H1,
99 after: ["Reach for primitives."],
100 });
101 expect(removed).toContain(">Reach for primitives.</h1>");
102 const inserted = applyEdit(PAGE, {
103 index: 0,
104 before: H1,
105 after: ["Reach for the very best better primitives."],
106 });
107 expect(inserted).toContain(
108 ">Reach for the very best better primitives.</h1>",
109 );
110 });
111
112 test("returns the source unchanged for whitespace-only edits", () => {
113 const out = applyEdit(PAGE, {
114 index: 0,
115 before: H1,
116 after: [" Reach for the better primitives. "],
117 });
118 expect(out).toBe(PAGE);
119 });
120
121 test("rejects stale text", () => {
122 expect(() =>
123 applyEdit(PAGE, { index: 0, before: ["Something else"], after: ["x"] }),
124 ).toThrow(/doesn't match the source/);
125 });
126
127 test("rejects edits that change markup", () => {
128 expect(() =>
129 applyEdit(PAGE, { index: 1, before: LEDE, after: ["only one node"] }),
130 ).toThrow(/removed or added markup/);
131 });
132
133 test("rejects elements containing expressions", () => {
134 expect(() =>
135 applyEdit(PAGE, { index: 2, before: ["x"], after: ["y"] }),
136 ).toThrow(/expressions/);
137 });
138});