@createiq/htmldiff 1.0.5-beta.0 → 1.0.5-beta.1

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.
@@ -0,0 +1,244 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { bench, describe, expect } from 'vitest'
3
+ import HtmlDiff from '../src/HtmlDiff'
4
+
5
+ // Helpers for generating synthetic table fixtures inline so each scenario
6
+ // is self-describing and doesn't require external HTML files.
7
+
8
+ function buildTable(rows: number, cols: number, cellContent: (r: number, c: number) => string): string {
9
+ const out: string[] = ['<table>']
10
+ for (let r = 0; r < rows; r++) {
11
+ out.push('<tr>')
12
+ for (let c = 0; c < cols; c++) {
13
+ out.push(`<td>${cellContent(r, c)}</td>`)
14
+ }
15
+ out.push('</tr>')
16
+ }
17
+ out.push('</table>')
18
+ return out.join('')
19
+ }
20
+
21
+ function defaultCell(r: number, c: number): string {
22
+ return `Row ${r} Col ${c} content with a few words to diff`
23
+ }
24
+
25
+ function nTables(n: number, rows: number, cols: number, prefix: string): string {
26
+ const out: string[] = []
27
+ for (let t = 0; t < n; t++) {
28
+ out.push(`<h2>Table ${t}</h2>`)
29
+ out.push(buildTable(rows, cols, (r, c) => `${prefix} t${t} r${r} c${c}`))
30
+ }
31
+ return out.join('')
32
+ }
33
+
34
+ const benchOptions = { iterations: 20, warmupIterations: 5 } as const
35
+
36
+ describe('TableDiff — small table baseline', () => {
37
+ const oldHtml = buildTable(10, 3, defaultCell)
38
+ const newHtml = oldHtml
39
+
40
+ bench(
41
+ 'small 10x3 — no changes',
42
+ () => {
43
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
44
+ },
45
+ benchOptions
46
+ )
47
+ })
48
+
49
+ describe('TableDiff — small table single edit', () => {
50
+ const oldHtml = buildTable(10, 3, defaultCell)
51
+ const newHtml = buildTable(10, 3, (r, c) => (r === 4 && c === 1 ? 'Edited content here' : defaultCell(r, c)))
52
+
53
+ bench(
54
+ 'small 10x3 — single cell edit',
55
+ () => {
56
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
57
+ },
58
+ benchOptions
59
+ )
60
+ })
61
+
62
+ describe('TableDiff — small table content shift across cells', () => {
63
+ // Shift content one cell to the right within each row: tests cell-by-cell
64
+ // positional preprocessing (the most-common row-shift mistake).
65
+ const cells: string[][] = []
66
+ for (let r = 0; r < 10; r++) {
67
+ const row: string[] = []
68
+ for (let c = 0; c < 3; c++) row.push(defaultCell(r, c))
69
+ cells.push(row)
70
+ }
71
+ const oldHtml = buildTable(10, 3, (r, c) => cells[r][c])
72
+ const newHtml = buildTable(10, 3, (r, c) => (c === 0 ? '' : cells[r][c - 1]))
73
+
74
+ bench(
75
+ 'small 10x3 — content shifted right',
76
+ () => {
77
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
78
+ },
79
+ benchOptions
80
+ )
81
+ })
82
+
83
+ describe('TableDiff — row added', () => {
84
+ const oldHtml = buildTable(10, 3, defaultCell)
85
+ const newHtml = buildTable(11, 3, (r, c) =>
86
+ r < 5 ? defaultCell(r, c) : r === 5 ? `Inserted row col ${c}` : defaultCell(r - 1, c)
87
+ )
88
+
89
+ bench(
90
+ 'small 10→11 rows × 3 cols — row added',
91
+ () => {
92
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
93
+ },
94
+ benchOptions
95
+ )
96
+ })
97
+
98
+ describe('TableDiff — column added', () => {
99
+ const oldHtml = buildTable(10, 3, defaultCell)
100
+ const newHtml = buildTable(10, 4, (r, c) =>
101
+ c < 2 ? defaultCell(r, c) : c === 2 ? `Inserted col cell r${r}` : defaultCell(r, c - 1)
102
+ )
103
+
104
+ bench(
105
+ 'small 10×3→10×4 — column added',
106
+ () => {
107
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
108
+ },
109
+ benchOptions
110
+ )
111
+ })
112
+
113
+ describe('TableDiff — horizontal merge (colspan)', () => {
114
+ // Old: <tr><td>a</td><td>b</td><td>c</td></tr>
115
+ // New: <tr><td colspan=2>a b</td><td>c</td></tr>
116
+ const oldHtml = '<table><tr><td>Apple</td><td>Banana</td><td>Cherry</td></tr></table>'
117
+ const newHtml = '<table><tr><td colspan="2">Apple Banana</td><td>Cherry</td></tr></table>'
118
+
119
+ bench(
120
+ 'horizontal merge — single row colspan',
121
+ () => {
122
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
123
+ },
124
+ benchOptions
125
+ )
126
+ })
127
+
128
+ describe('TableDiff — vertical merge (rowspan)', () => {
129
+ // Old: 3 rows of single cells; New: row 0 has rowspan=3, rows 1-2 empty.
130
+ const oldHtml = '<table><tr><td>One</td></tr><tr><td>Two</td></tr><tr><td>Three</td></tr></table>'
131
+ const newHtml = '<table><tr><td rowspan="3">One Two Three</td></tr><tr></tr><tr></tr></table>'
132
+
133
+ bench(
134
+ 'vertical merge — single col rowspan',
135
+ () => {
136
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
137
+ },
138
+ benchOptions
139
+ )
140
+ })
141
+
142
+ describe('TableDiff — medium table single edit', () => {
143
+ const oldHtml = buildTable(50, 10, defaultCell)
144
+ const newHtml = buildTable(50, 10, (r, c) => (r === 25 && c === 5 ? 'Edited mid-table' : defaultCell(r, c)))
145
+
146
+ bench(
147
+ 'medium 50x10 — single edit',
148
+ () => {
149
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
150
+ },
151
+ benchOptions
152
+ )
153
+ })
154
+
155
+ describe('TableDiff — large table stress test', () => {
156
+ const oldHtml = buildTable(500, 5, defaultCell)
157
+ const newHtml = buildTable(500, 5, (r, c) => (r === 250 && c === 2 ? 'Edited deep row' : defaultCell(r, c)))
158
+
159
+ bench(
160
+ 'large 500x5 — single edit',
161
+ () => {
162
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
163
+ },
164
+ { iterations: 5, warmupIterations: 2 }
165
+ )
166
+ })
167
+
168
+ describe('TableDiff — wide table stress test', () => {
169
+ const oldHtml = buildTable(5, 50, defaultCell)
170
+ const newHtml = buildTable(5, 50, (r, c) => (r === 2 && c === 25 ? 'Edited wide table' : defaultCell(r, c)))
171
+
172
+ bench(
173
+ 'wide 5x50 — single edit',
174
+ () => {
175
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
176
+ },
177
+ benchOptions
178
+ )
179
+ })
180
+
181
+ describe('TableDiff — many tables in one input', () => {
182
+ const oldHtml = nTables(5, 10, 3, 'old')
183
+ // Edit one cell in each of the 5 tables to exercise preprocessTables iteration.
184
+ const newHtml = nTables(5, 10, 3, 'new')
185
+
186
+ bench(
187
+ '5 tables × 10x3 — all-cells edited',
188
+ () => {
189
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
190
+ },
191
+ benchOptions
192
+ )
193
+ })
194
+
195
+ describe('TableDiff — real-world fixture (input1 vs input2)', () => {
196
+ const input1 = readFileSync('test/input1.html', 'utf8')
197
+ const input2 = readFileSync('test/input2.html', 'utf8')
198
+
199
+ bench(
200
+ 'input1.html vs input2.html',
201
+ () => {
202
+ expect(HtmlDiff.execute(input1, input2)).toBeTruthy()
203
+ },
204
+ { iterations: 3, warmupIterations: 1 }
205
+ )
206
+ })
207
+
208
+ describe('TableDiff — pathological 100x100 sparse edits', () => {
209
+ // 100x100 = 10,000 cells. Sparse edits (5 cells) — checks per-cell
210
+ // recursion overhead and lcsAlign DP cost when dimensions match.
211
+ const oldHtml = buildTable(100, 100, defaultCell)
212
+ const newHtml = buildTable(100, 100, (r, c) => {
213
+ const edited =
214
+ (r === 0 && c === 0) ||
215
+ (r === 50 && c === 50) ||
216
+ (r === 99 && c === 99) ||
217
+ (r === 25 && c === 75) ||
218
+ (r === 75 && c === 25)
219
+ return edited ? `Edited r${r} c${c}` : defaultCell(r, c)
220
+ })
221
+
222
+ bench(
223
+ '100x100 — sparse edits (same dimensions, positional path)',
224
+ () => {
225
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
226
+ },
227
+ { iterations: 3, warmupIterations: 1 }
228
+ )
229
+ })
230
+
231
+ describe('TableDiff — pathological 100x100 row-LCS', () => {
232
+ // 100x100 with a row deletion — forces lcsAlign over 100 row keys, each
233
+ // ~3-4KB long after content. Tests row-key whitespace-normalization cost.
234
+ const oldHtml = buildTable(100, 100, defaultCell)
235
+ const newHtml = buildTable(99, 100, (r, c) => defaultCell(r < 50 ? r : r + 1, c))
236
+
237
+ bench(
238
+ '100x100 → 99x100 — row deleted (row-LCS path)',
239
+ () => {
240
+ expect(HtmlDiff.execute(oldHtml, newHtml)).toBeTruthy()
241
+ },
242
+ { iterations: 3, warmupIterations: 1 }
243
+ )
244
+ })