@pi-archimedes/diff 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ansi/manip.test.ts +213 -0
- package/src/ansi/width.test.ts +95 -0
- package/src/word-diff.test.ts +111 -0
package/package.json
CHANGED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { strip, tabs, fit, ansiState, isLowContrastShikiFg, normalizeShikiContrast, lnum, shortPath, summarize } from "./manip.js";
|
|
3
|
+
import * as C from "./codes.js";
|
|
4
|
+
|
|
5
|
+
// ── strip ───────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
describe("strip", () => {
|
|
8
|
+
it("removes SGR sequences", () => {
|
|
9
|
+
expect(strip("\x1b[31mred\x1b[0m")).toBe("red");
|
|
10
|
+
expect(strip("\x1b[1m\x1b[31mhi\x1b[0m")).toBe("hi");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("empty string stays empty", () => {
|
|
14
|
+
expect(strip("")).toBe("");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// ── tabs ────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
describe("tabs", () => {
|
|
21
|
+
it("replaces \\t with 2 spaces", () => {
|
|
22
|
+
expect(tabs("a\tb")).toBe("a b");
|
|
23
|
+
expect(tabs("\t")).toBe(" ");
|
|
24
|
+
expect(tabs("no\ttabs\there")).toBe("no tabs here");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ── fit ─────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
describe("fit", () => {
|
|
31
|
+
it("truncates to width (ASCII)", () => {
|
|
32
|
+
const result = fit("hello world", 8);
|
|
33
|
+
expect(result).toContain("hello");
|
|
34
|
+
// Should end with truncation indicator
|
|
35
|
+
expect(result).toContain("›");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("truncates to width (wide chars — never splits grapheme)", () => {
|
|
39
|
+
// "中日" is 4 columns. fit to 3 should drop "日" (2 cols) since it
|
|
40
|
+
// would overflow, keeping only "中" (2 cols)
|
|
41
|
+
const result = fit("中日", 3);
|
|
42
|
+
expect(result).toContain("中");
|
|
43
|
+
// "日" should not appear since it's 2 cols and would overflow budget of 2 (3-1)
|
|
44
|
+
expect(result).not.toContain("日");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("pads short strings", () => {
|
|
48
|
+
const result = fit("hi", 5);
|
|
49
|
+
expect(result).toBe("hi ");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("width 0 returns empty", () => {
|
|
53
|
+
expect(fit("hello", 0)).toBe("");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("wide grapheme wider than budget is dropped", () => {
|
|
57
|
+
// Budget of 1 — wide char "中" (2 cols) can't fit, should be dropped
|
|
58
|
+
const result = fit("中", 1);
|
|
59
|
+
expect(result).not.toContain("中");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ── ansiState ───────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
describe("ansiState", () => {
|
|
66
|
+
it("extracts last fg code (truecolor)", () => {
|
|
67
|
+
const state = ansiState("\x1b[38;2;255;0;0mhello");
|
|
68
|
+
expect(state).toContain("\x1b[38;2;255;0;0m");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("extracts last bg code (truecolor)", () => {
|
|
72
|
+
const state = ansiState("\x1b[48;2;0;255;0mhello");
|
|
73
|
+
expect(state).toContain("\x1b[48;2;0;255;0m");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns bg + fg (background first!)", () => {
|
|
77
|
+
const state = ansiState("\x1b[38;2;255;0;0m\x1b[48;2;0;0;255mtext");
|
|
78
|
+
// bg should come before fg in the return value
|
|
79
|
+
const bgIdx = state.indexOf("\x1b[48;2;0;0;255m");
|
|
80
|
+
const fgIdx = state.indexOf("\x1b[38;2;255;0;0m");
|
|
81
|
+
expect(bgIdx).toBeLessThan(fgIdx);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("\\x1b[0m resets both", () => {
|
|
85
|
+
const state = ansiState("\x1b[38;2;255;0;0m\x1b[0m");
|
|
86
|
+
expect(state).toBe("");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("truecolor fg/bg", () => {
|
|
90
|
+
const state = ansiState("\x1b[38;2;255;0;0m\x1b[48;2;0;0;255mtext");
|
|
91
|
+
expect(state).toContain("\x1b[38;2;255;0;0m");
|
|
92
|
+
expect(state).toContain("\x1b[48;2;0;0;255m");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("\\x1b[39m resets only fg", () => {
|
|
96
|
+
const state = ansiState("\x1b[38;2;255;0;0m\x1b[48;2;0;0;255m\x1b[39mtext");
|
|
97
|
+
expect(state).toContain("\x1b[48;2;0;0;255m");
|
|
98
|
+
expect(state).not.toContain("\x1b[38;2;255;0;0m");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("plain ANSI codes (31, 42) are not tracked — Shiki uses truecolor", () => {
|
|
102
|
+
// ansiState only tracks 38;..., 48;..., 39, 0 — plain codes like 31/42 pass through
|
|
103
|
+
const state = ansiState("\x1b[31m\x1b[42mhello");
|
|
104
|
+
expect(state).toBe("");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ── isLowContrastShikiFg ────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
describe("isLowContrastShikiFg", () => {
|
|
111
|
+
it('"30" = true (dark black)', () => {
|
|
112
|
+
expect(isLowContrastShikiFg("30")).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('"90" = true (dark gray)', () => {
|
|
116
|
+
expect(isLowContrastShikiFg("90")).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('"38;5;0" = true (256 palette black)', () => {
|
|
120
|
+
expect(isLowContrastShikiFg("38;5;0")).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('"38;5;8" = true (256 palette dark gray)', () => {
|
|
124
|
+
expect(isLowContrastShikiFg("38;5;8")).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('"38;2;0;0;0" = true (truecolor black)', () => {
|
|
128
|
+
expect(isLowContrastShikiFg("38;2;0;0;0")).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('"38;2;255;255;255" = false (truecolor white)', () => {
|
|
132
|
+
expect(isLowContrastShikiFg("38;2;255;255;255")).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('"0" = false (reset, not a fg code)', () => {
|
|
136
|
+
expect(isLowContrastShikiFg("0")).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('"38;5;5" = false (256 palette not-dark)', () => {
|
|
140
|
+
expect(isLowContrastShikiFg("38;5;5")).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('"48;2;0;0;0" = false (background not foreground)', () => {
|
|
144
|
+
expect(isLowContrastShikiFg("48;2;0;0;0")).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ── normalizeShikiContrast ──────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
describe("normalizeShikiContrast", () => {
|
|
151
|
+
it("replaces low-contrast codes", () => {
|
|
152
|
+
// \x1b[30m is dark black — should be replaced
|
|
153
|
+
const result = normalizeShikiContrast("\x1b[30mtext\x1b[0m");
|
|
154
|
+
expect(result).toContain(C.FG_SAFE_MUTED);
|
|
155
|
+
expect(result).not.toContain("\x1b[30m");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("leaves high-contrast alone", () => {
|
|
159
|
+
// \x1b[37m is white — should pass through
|
|
160
|
+
const result = normalizeShikiContrast("\x1b[37mtext\x1b[0m");
|
|
161
|
+
expect(result).toContain("\x1b[37m");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── lnum ────────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
describe("lnum", () => {
|
|
168
|
+
it("formats number right-padded", () => {
|
|
169
|
+
const result = lnum(42, 5);
|
|
170
|
+
expect(result).toContain("42");
|
|
171
|
+
// Should have spaces before the number
|
|
172
|
+
const stripped = result.replace(C.RST, "").replace(C.FG_LNUM, "");
|
|
173
|
+
expect(stripped).toBe(" 42");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("null produces spaces", () => {
|
|
177
|
+
const result = lnum(null, 5);
|
|
178
|
+
expect(result).toBe(" ");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ── shortPath ───────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
describe("shortPath", () => {
|
|
185
|
+
it("relative to cwd returns relative", () => {
|
|
186
|
+
const result = shortPath("/home/user/project", "/home/user", "/home/user/project/src/file.ts");
|
|
187
|
+
expect(result).toBe("src/file.ts");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("outside cwd with home returns ~", () => {
|
|
191
|
+
const result = shortPath("/home/user/project", "/home/user", "/home/user/other/file.ts");
|
|
192
|
+
expect(result).toBe("~/other/file.ts");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("empty returns empty", () => {
|
|
196
|
+
expect(shortPath("/cwd", "/home", "")).toBe("");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ── summarize ───────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
describe("summarize", () => {
|
|
203
|
+
it("+N -M format", () => {
|
|
204
|
+
const result = summarize(3, 2);
|
|
205
|
+
expect(result).toContain("+3");
|
|
206
|
+
expect(result).toContain("-2");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("zero changes returns 'no changes'", () => {
|
|
210
|
+
const result = summarize(0, 0);
|
|
211
|
+
expect(result).toContain("no changes");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { graphemeWidth, tokenize, visibleWidth } from "./width.js";
|
|
3
|
+
|
|
4
|
+
// ── graphemeWidth ───────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
describe("graphemeWidth", () => {
|
|
7
|
+
it("ASCII letter = 1", () => {
|
|
8
|
+
expect(graphemeWidth("a")).toBe(1);
|
|
9
|
+
expect(graphemeWidth("Z")).toBe(1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("CJK character = 2", () => {
|
|
13
|
+
expect(graphemeWidth("中")).toBe(2);
|
|
14
|
+
expect(graphemeWidth("日")).toBe(2);
|
|
15
|
+
expect(graphemeWidth("韩")).toBe(2);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("emoji = 2", () => {
|
|
19
|
+
expect(graphemeWidth("✅")).toBe(2);
|
|
20
|
+
expect(graphemeWidth("🔥")).toBe(2);
|
|
21
|
+
expect(graphemeWidth("😀")).toBe(2);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("zero-width combining marks = 0", () => {
|
|
25
|
+
// Combining acute accent (U+0301) — zero width on its own
|
|
26
|
+
expect(graphemeWidth("\u0301")).toBe(0);
|
|
27
|
+
// Combining tilde (U+0303)
|
|
28
|
+
expect(graphemeWidth("\u0303")).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("tab = 2", () => {
|
|
32
|
+
expect(graphemeWidth("\t")).toBe(2);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ── tokenize ────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe("tokenize", () => {
|
|
39
|
+
it("plain text produces single tokens", () => {
|
|
40
|
+
const tokens = tokenize("ab");
|
|
41
|
+
expect(tokens).toHaveLength(2);
|
|
42
|
+
expect(tokens[0]).toEqual({ ansi: "", text: "a", width: 1 });
|
|
43
|
+
expect(tokens[1]).toEqual({ ansi: "", text: "b", width: 1 });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("SGR sequence produces ansi+text pairs", () => {
|
|
47
|
+
// \x1b[31m is red fg, followed by "a"
|
|
48
|
+
const tokens = tokenize("\x1b[31ma");
|
|
49
|
+
expect(tokens).toHaveLength(1);
|
|
50
|
+
expect(tokens[0]).toEqual({ ansi: "\x1b[31m", text: "a", width: 1 });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("bare ESC without m terminator handled gracefully", () => {
|
|
54
|
+
// ESC (0x1b) with no 'm' after it — should not hang
|
|
55
|
+
const tokens = tokenize("\x1ba");
|
|
56
|
+
// The bare ESC produces a zero-width token, then "a" is a normal token
|
|
57
|
+
expect(tokens.length).toBeGreaterThanOrEqual(2);
|
|
58
|
+
// Last token should be the "a"
|
|
59
|
+
expect(tokens[tokens.length - 1]!.text).toBe("a");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("mixed ANSI + text", () => {
|
|
63
|
+
const tokens = tokenize("\x1b[31mred\x1b[0mplain");
|
|
64
|
+
// "red" under \x1b[31m, then "plain" after reset
|
|
65
|
+
const redToken = tokens.find((t) => t.text === "r");
|
|
66
|
+
expect(redToken?.ansi).toBe("\x1b[31m");
|
|
67
|
+
|
|
68
|
+
const plainToken = tokens.find((t) => t.text === "p");
|
|
69
|
+
expect(plainToken?.ansi).toBe("\x1b[0m");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ── visibleWidth ────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
describe("visibleWidth", () => {
|
|
76
|
+
it("plain ASCII length matches", () => {
|
|
77
|
+
expect(visibleWidth("hello")).toBe(5);
|
|
78
|
+
expect(visibleWidth("abc")).toBe(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("wide chars counted correctly", () => {
|
|
82
|
+
expect(visibleWidth("中")).toBe(2);
|
|
83
|
+
expect(visibleWidth("中日")).toBe(4);
|
|
84
|
+
expect(visibleWidth("a中b")).toBe(4); // 1 + 2 + 1
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("ANSI escapes don't add width", () => {
|
|
88
|
+
expect(visibleWidth("\x1b[31mhello\x1b[0m")).toBe(5);
|
|
89
|
+
expect(visibleWidth("\x1b[1m\x1b[31mhi\x1b[0m")).toBe(2);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("empty string = 0", () => {
|
|
93
|
+
expect(visibleWidth("")).toBe(0);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { wordDiffAnalysis, injectBg, plainWordDiff } from "./word-diff.js";
|
|
3
|
+
import * as Ansi from "./ansi/index.js";
|
|
4
|
+
import type { DiffBg } from "./ansi/index.js";
|
|
5
|
+
|
|
6
|
+
// ── wordDiffAnalysis ────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
describe("wordDiffAnalysis", () => {
|
|
9
|
+
it("identical strings → similarity 1, empty ranges", () => {
|
|
10
|
+
const result = wordDiffAnalysis("hello world", "hello world");
|
|
11
|
+
expect(result.similarity).toBe(1);
|
|
12
|
+
expect(result.oldRanges).toEqual([]);
|
|
13
|
+
expect(result.newRanges).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("completely different → similarity 0, full ranges", () => {
|
|
17
|
+
const result = wordDiffAnalysis("abc", "xyz");
|
|
18
|
+
expect(result.similarity).toBe(0);
|
|
19
|
+
expect(result.oldRanges).toEqual([[0, 3]]);
|
|
20
|
+
expect(result.newRanges).toEqual([[0, 3]]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("partial overlap → correct similarity and ranges", () => {
|
|
24
|
+
const result = wordDiffAnalysis("hello world", "hello earth");
|
|
25
|
+
expect(result.similarity).toBeGreaterThan(0);
|
|
26
|
+
expect(result.similarity).toBeLessThan(1);
|
|
27
|
+
// "world" changed to "earth" — both 5 chars, same position in new string
|
|
28
|
+
expect(result.oldRanges.length).toBeGreaterThan(0);
|
|
29
|
+
expect(result.newRanges.length).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("one empty string", () => {
|
|
33
|
+
const result = wordDiffAnalysis("hello", "");
|
|
34
|
+
expect(result.similarity).toBe(0);
|
|
35
|
+
expect(result.oldRanges).toEqual([[0, 5]]);
|
|
36
|
+
expect(result.newRanges).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("both empty → similarity 1", () => {
|
|
40
|
+
const result = wordDiffAnalysis("", "");
|
|
41
|
+
expect(result.similarity).toBe(1);
|
|
42
|
+
expect(result.oldRanges).toEqual([]);
|
|
43
|
+
expect(result.newRanges).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ── injectBg ────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
describe("injectBg", () => {
|
|
50
|
+
const baseBg = "\x1b[48;2;10;20;30m";
|
|
51
|
+
const hlBg = "\x1b[48;2;200;50;50m";
|
|
52
|
+
|
|
53
|
+
it("no ranges returns baseBg + ansiLine + Ansi.RST (trailing reset!)", () => {
|
|
54
|
+
const result = injectBg("hello", [], baseBg, hlBg);
|
|
55
|
+
expect(result).toBe(baseBg + "hello" + Ansi.RST);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("single range with concrete example", () => {
|
|
59
|
+
// "hello" with highlight on [2,4] → "he" + highlight "ll" + "o"
|
|
60
|
+
const result = injectBg("hello", [[2, 4]], baseBg, hlBg);
|
|
61
|
+
expect(result).toBe(
|
|
62
|
+
baseBg + "he" + hlBg + "ll" + baseBg + "o" + Ansi.RST
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("overlapping ranges merged", () => {
|
|
67
|
+
// [[1, 4], [3, 6]] should merge to [[1, 6]]
|
|
68
|
+
const result = injectBg("abcdef", [[1, 4], [3, 6]], baseBg, hlBg);
|
|
69
|
+
// "a" is base, "bcdef" is highlighted
|
|
70
|
+
expect(result).toBe(
|
|
71
|
+
baseBg + "a" + hlBg + "bcdef" + Ansi.RST
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("\\x1b[0m triggers bg re-injection", () => {
|
|
76
|
+
// When a reset appears mid-string, bg should be re-injected after it
|
|
77
|
+
const ansiLine = "he\x1b[0mllo";
|
|
78
|
+
const result = injectBg(ansiLine, [[2, 4]], baseBg, hlBg);
|
|
79
|
+
// After \x1b[0m, the bg should be re-injected
|
|
80
|
+
expect(result).toContain(baseBg);
|
|
81
|
+
// The reset should still be present
|
|
82
|
+
expect(result).toContain("\x1b[0m");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ── plainWordDiff ───────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
describe("plainWordDiff", () => {
|
|
89
|
+
const dbg: DiffBg = Ansi.DEFAULT_DIFF_BG;
|
|
90
|
+
|
|
91
|
+
it("removed text gets del bg", () => {
|
|
92
|
+
const result = plainWordDiff("hello world", "hello", dbg);
|
|
93
|
+
expect(result.old).toContain(dbg.bgDelW);
|
|
94
|
+
expect(result.old).toContain("world");
|
|
95
|
+
expect(result.new).toBe("hello");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("added text gets add bg", () => {
|
|
99
|
+
const result = plainWordDiff("hello", "hello world", dbg);
|
|
100
|
+
expect(result.new).toContain(dbg.bgAddW);
|
|
101
|
+
expect(result.new).toContain("world");
|
|
102
|
+
// diffWords treats the space as unchanged, so old includes trailing space
|
|
103
|
+
expect(result.old).toBe("hello ");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("unchanged passed through", () => {
|
|
107
|
+
const result = plainWordDiff("hello", "hello", dbg);
|
|
108
|
+
expect(result.old).toBe("hello");
|
|
109
|
+
expect(result.new).toBe("hello");
|
|
110
|
+
});
|
|
111
|
+
});
|