This repository has no description
1<script module lang="ts">
2 import { defineMeta, type StoryContext } from "@storybook/addon-svelte-csf";
3 import { expect, waitFor } from "storybook/test";
4 import { LineOp, type DiffFile } from "$lib/api/diff";
5 import { toFileDiffMetadata } from "./fileDiff";
6 import PierreDiff from "./PierreDiff.svelte";
7
8 const context = (line: string) => ({ Op: LineOp.Context, Line: `${line}\n` });
9 const deletion = (line: string) => ({ Op: LineOp.Delete, Line: `${line}\n` });
10 const addition = (line: string) => ({ Op: LineOp.Add, Line: `${line}\n` });
11
12 const files: DiffFile[] = [
13 {
14 name: { old: "src/colors.ts", new: "src/colors.ts" },
15 text_fragments: [
16 {
17 OldPosition: 1,
18 OldLines: 4,
19 NewPosition: 1,
20 NewLines: 6,
21 Lines: [
22 context("export const palette = {"),
23 deletion('\tbackground: "white",'),
24 deletion('\tforeground: "black",'),
25 addition('\tbackground: "oklch(0.98 0 0)",'),
26 addition('\tforeground: "oklch(0.2 0 0)",'),
27 context("};"),
28 addition(""),
29 addition('export const accent = "oklch(0.6 0.2 250)";')
30 ]
31 },
32 {
33 OldPosition: 10,
34 OldLines: 3,
35 NewPosition: 11,
36 NewLines: 3,
37 Lines: [
38 context("export const spacing = 4;"),
39 deletion("export const radius = 0;"),
40 addition("export const radius = 8;"),
41 context('export const shadow = "none";')
42 ]
43 }
44 ]
45 },
46 {
47 name: { old: "src/old.ts", new: "src/renamed.ts" },
48 is_rename: true,
49 text_fragments: [
50 {
51 OldPosition: 1,
52 OldLines: 2,
53 NewPosition: 1,
54 NewLines: 2,
55 Lines: [
56 deletion('export const name = "old";'),
57 addition('export const name = "renamed";'),
58 context("export default name;")
59 ]
60 }
61 ]
62 }
63 ];
64
65 // the hunk starts deep in the file, the leading unmodified region collapses into a
66 // top separator
67 const offsetFiles: DiffFile[] = [
68 {
69 name: { old: "src/deep.ts", new: "src/deep.ts" },
70 text_fragments: [
71 {
72 OldPosition: 20,
73 OldLines: 3,
74 NewPosition: 20,
75 NewLines: 3,
76 Lines: [
77 context("export const spacing = 4;"),
78 deletion("export const radius = 0;"),
79 addition("export const radius = 8;"),
80 context('export const shadow = "none";')
81 ]
82 }
83 ]
84 }
85 ];
86
87 type PlayContext = Pick<StoryContext<Record<string, unknown>>, "canvasElement">;
88
89 const hostsOf = (canvasElement: HTMLElement) => [
90 ...canvasElement.querySelectorAll("diffs-container")
91 ];
92
93 // pierre renders into shadow roots, testing library cannot see through
94 const shadowText = (canvasElement: HTMLElement) =>
95 hostsOf(canvasElement)
96 .map((host) => host.shadowRoot?.textContent ?? "")
97 .join("\n");
98
99 const rendersPatch = async (canvasElement: HTMLElement) => {
100 const hosts = hostsOf(canvasElement);
101 await expect(hosts.length).toBe(2);
102 await waitFor(() => expect(shadowText(canvasElement)).toContain("export const palette"));
103 await waitFor(() => expect(shadowText(canvasElement)).toContain("export const name"));
104 return hosts;
105 };
106
107 const appviewChrome = async ({ canvasElement }: PlayContext) => {
108 const hosts = await rendersPatch(canvasElement);
109 for (const host of hosts) {
110 expect(host.shadowRoot?.querySelector("[data-diffs-header]")).toBeNull();
111 expect(host.shadowRoot?.querySelector("[data-indicators='classic']")).not.toBeNull();
112 }
113 // the hunks cover old lines 1-5 and 10-12, 4 collapse between
114 expect(hosts[0].shadowRoot?.querySelector("[data-separator='line-info']")).not.toBeNull();
115 await waitFor(() => expect(shadowText(canvasElement)).toContain("4 unmodified lines"));
116 };
117
118 const expectSplitColumns = (present: boolean) => {
119 return async ({ canvasElement }: PlayContext) => {
120 const hosts = await rendersPatch(canvasElement);
121 await waitFor(() => {
122 const found = hosts.some(
123 (host) =>
124 host.shadowRoot?.querySelector("[data-deletions]") &&
125 host.shadowRoot?.querySelector("[data-additions]")
126 );
127 expect(found).toBe(present);
128 });
129 };
130 };
131
132 // the separator bar must span the diff instead of clipping in the 44px
133 // gutter. the additions-side copy hides its text, only measure
134 // non-zero spans
135 const separatorNotClipped = async ({ canvasElement }: PlayContext) => {
136 const hosts = await rendersPatch(canvasElement);
137 await waitFor(() => {
138 const spans = hosts.flatMap((host) => [
139 ...(host.shadowRoot?.querySelectorAll("[data-unmodified-lines]") ?? [])
140 ]);
141 const visible = spans.filter((span) => span.getBoundingClientRect().width > 0);
142 expect(visible.length).toBeGreaterThan(0);
143 for (const span of visible) {
144 const wrapper = span.closest("[data-separator-wrapper]");
145 expect(wrapper).not.toBeNull();
146 const wrapperRect = wrapper!.getBoundingClientRect();
147 const spanRect = span.getBoundingClientRect();
148 expect(wrapperRect.width).toBeGreaterThan(200);
149 expect(spanRect.x).toBeGreaterThanOrEqual(wrapperRect.x - 1);
150 expect(spanRect.right).toBeLessThanOrEqual(wrapperRect.right + 1);
151 }
152 });
153 };
154
155 const { Story } = defineMeta({
156 title: "Repo/PierreDiff",
157 component: PierreDiff,
158 tags: ["autodocs"],
159 args: { files: files.map(toFileDiffMetadata) }
160 });
161</script>
162
163<Story
164 name="Unified"
165 play={async (ctx) => {
166 await appviewChrome(ctx);
167 await expectSplitColumns(false)(ctx);
168 await separatorNotClipped(ctx);
169 }}
170/>
171
172<Story
173 name="Split"
174 args={{ diffStyle: "split" }}
175 play={async (ctx) => {
176 await expectSplitColumns(true)(ctx);
177 await separatorNotClipped(ctx);
178 }}
179/>
180
181<Story
182 name="Leading collapse"
183 args={{ files: offsetFiles.map(toFileDiffMetadata) }}
184 play={async ({ canvasElement }: PlayContext) => {
185 await waitFor(() => expect(shadowText(canvasElement)).toContain("19 unmodified lines"));
186 const host = hostsOf(canvasElement)[0];
187 const separator = host.shadowRoot?.querySelector("[data-separator='line-info']");
188 expect(separator).not.toBeNull();
189 expect(separator?.hasAttribute("data-separator-first")).toBe(true);
190 }}
191/>