@createiq/htmldiff 1.2.0-beta.0 → 1.2.0-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.
@@ -2,67 +2,70 @@ import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import HtmlDiff from '../src/HtmlDiff'
4
4
 
5
- describe('HtmlDiff.executeThreeWay (tables, positional case)', () => {
6
- describe('cell-level attribution propagates through table scaffolding', () => {
7
- it('CP-only edit inside a cell', () => {
5
+ /**
6
+ * Table tests under the genesis-spine model.
7
+ *
8
+ * `executeThreeWay(genesis, cpLatest, meCurrent)` walks the genesis
9
+ * table structure as the spine and attributes cp/me changes
10
+ * independently. Tables/rows/cells that both authors changed identically
11
+ * settle and emit plain; one-sided changes carry author attribution;
12
+ * disagreements show both authors' contributions.
13
+ */
14
+ describe('HtmlDiff.executeThreeWay (tables, genesis-spine)', () => {
15
+ describe('cell-level attribution through table scaffolding', () => {
16
+ it('CP only changed, Me kept genesis → cp attribution', () => {
8
17
  expect(
9
18
  HtmlDiff.executeThreeWay(
10
- '<table><tr><td>Rate</td><td>five</td></tr></table>',
11
- '<table><tr><td>Interest Rate</td><td>five</td></tr></table>',
12
- '<table><tr><td>Interest Rate</td><td>five</td></tr></table>'
19
+ '<table><tr><td>Rate</td></tr></table>',
20
+ '<table><tr><td>Interest Rate</td></tr></table>',
21
+ '<table><tr><td>Rate</td></tr></table>'
13
22
  )
14
- ).toBe(
15
- "<table><tr><td><ins class='diffins cp' data-author='cp'>Interest </ins>Rate</td><td>five</td></tr></table>"
16
- )
23
+ ).toBe("<table><tr><td><ins class='diffins cp' data-author='cp'>Interest </ins>Rate</td></tr></table>")
17
24
  })
18
25
 
19
- it('Me-only edit inside a cell', () => {
26
+ it('Me only changed, CP kept genesis → me attribution', () => {
20
27
  expect(
21
28
  HtmlDiff.executeThreeWay(
22
- '<table><tr><td>Rate</td><td>five</td></tr></table>',
23
- '<table><tr><td>Rate</td><td>five</td></tr></table>',
24
- '<table><tr><td>Rate</td><td>five percent</td></tr></table>'
29
+ '<table><tr><td>Rate</td></tr></table>',
30
+ '<table><tr><td>Rate</td></tr></table>',
31
+ '<table><tr><td>Rate per annum</td></tr></table>'
25
32
  )
26
- ).toBe(
27
- "<table><tr><td>Rate</td><td>five<ins class='diffins me' data-author='me'>&nbsp;percent</ins></td></tr></table>"
28
- )
33
+ ).toBe("<table><tr><td>Rate<ins class='diffins me' data-author='me'>&nbsp;per annum</ins></td></tr></table>")
29
34
  })
30
35
 
31
- it('Me keeps a CP cell insertion AND adds their own to a different cell', () => {
36
+ it('Settled both made the same change no markup', () => {
32
37
  expect(
33
38
  HtmlDiff.executeThreeWay(
34
- '<table><tr><td>Rate</td><td>five</td></tr></table>',
35
- '<table><tr><td>Interest Rate</td><td>five</td></tr></table>',
36
- '<table><tr><td>Interest Rate</td><td>five percent</td></tr></table>'
39
+ '<table><tr><td>Rate</td></tr></table>',
40
+ '<table><tr><td>Interest Rate</td></tr></table>',
41
+ '<table><tr><td>Interest Rate</td></tr></table>'
37
42
  )
38
- ).toBe(
39
- "<table><tr><td><ins class='diffins cp' data-author='cp'>Interest </ins>Rate</td><td>five<ins class='diffins me' data-author='me'>&nbsp;percent</ins></td></tr></table>"
40
- )
43
+ ).toBe('<table><tr><td>Interest Rate</td></tr></table>')
41
44
  })
42
45
 
43
- it('Me rejects a CP cell insertion (reject markup inside the cell)', () => {
46
+ it('Disagreement different changes at the same place', () => {
44
47
  expect(
45
48
  HtmlDiff.executeThreeWay(
46
49
  '<table><tr><td>five</td></tr></table>',
47
50
  '<table><tr><td>five and a half</td></tr></table>',
48
- '<table><tr><td>five</td></tr></table>'
51
+ '<table><tr><td>seven</td></tr></table>'
49
52
  )
50
53
  ).toBe(
51
- "<table><tr><td>five<del class='diffdel me rejects-cp' data-author='me' data-rejects='cp'>&nbsp;and a half</del></td></tr></table>"
54
+ "<table><tr><td><del class='diffdel me' data-author='me'>five</del><ins class='diffins cp' data-author='cp'>&nbsp;and a half</ins><ins class='diffins me' data-author='me'>seven</ins></td></tr></table>"
52
55
  )
53
56
  })
54
57
 
55
- it('CP-deletion of V1 cell content', () => {
58
+ it('CP deletes content, Me kept', () => {
56
59
  expect(
57
60
  HtmlDiff.executeThreeWay(
58
61
  '<table><tr><td>five and a half</td></tr></table>',
59
62
  '<table><tr><td>five</td></tr></table>',
60
- '<table><tr><td>five</td></tr></table>'
63
+ '<table><tr><td>five and a half</td></tr></table>'
61
64
  )
62
65
  ).toBe("<table><tr><td>five<del class='diffdel cp' data-author='cp'>&nbsp;and a half</del></td></tr></table>")
63
66
  })
64
67
 
65
- it('Me-deletion of V1 cell content (CP did nothing)', () => {
68
+ it('Me deletes content, CP kept', () => {
66
69
  expect(
67
70
  HtmlDiff.executeThreeWay(
68
71
  '<table><tr><td>five and a half</td></tr></table>',
@@ -74,103 +77,74 @@ describe('HtmlDiff.executeThreeWay (tables, positional case)', () => {
74
77
  })
75
78
 
76
79
  describe('scaffolding scanner edge cases', () => {
77
- it('preserves <thead>/<tbody> wrappers', () => {
80
+ it('<thead>/<tbody> wrappers', () => {
78
81
  expect(
79
82
  HtmlDiff.executeThreeWay(
80
83
  '<table><thead><tr><th>Term</th></tr></thead><tbody><tr><td>Old</td></tr></tbody></table>',
81
84
  '<table><thead><tr><th>Term</th></tr></thead><tbody><tr><td>New</td></tr></tbody></table>',
82
- '<table><thead><tr><th>Term</th></tr></thead><tbody><tr><td>New</td></tr></tbody></table>'
85
+ '<table><thead><tr><th>Term</th></tr></thead><tbody><tr><td>Old</td></tr></tbody></table>'
83
86
  )
84
87
  ).toBe(
85
88
  "<table><thead><tr><th>Term</th></tr></thead><tbody><tr><td><del class='diffdel cp' data-author='cp'>Old</del><ins class='diffins cp' data-author='cp'>New</ins></td></tr></tbody></table>"
86
89
  )
87
90
  })
88
91
 
89
- it('handles <th> header cells correctly', () => {
90
- // Branches on td vs th in the scanner; the contentEnd computation
91
- // uses the right tagName length.
92
+ it('<th> header cells', () => {
92
93
  expect(
93
94
  HtmlDiff.executeThreeWay(
94
95
  '<table><tr><th>Label</th><th>Value</th></tr></table>',
95
96
  '<table><tr><th>Description</th><th>Value</th></tr></table>',
96
- '<table><tr><th>Description</th><th>Value</th></tr></table>'
97
+ '<table><tr><th>Label</th><th>Value</th></tr></table>'
97
98
  )
98
99
  ).toBe(
99
100
  "<table><tr><th><del class='diffdel cp' data-author='cp'>Label</del><ins class='diffins cp' data-author='cp'>Description</ins></th><th>Value</th></tr></table>"
100
101
  )
101
102
  })
102
103
 
103
- it('handles an empty cell (contentStart == contentEnd)', () => {
104
- // Empty cell on both sides — no diff, just scaffolding pass-through.
105
- expect(
106
- HtmlDiff.executeThreeWay(
107
- '<table><tr><td>a</td><td></td></tr></table>',
108
- '<table><tr><td>a</td><td></td></tr></table>',
109
- '<table><tr><td>a</td><td></td></tr></table>'
110
- )
111
- ).toBe('<table><tr><td>a</td><td></td></tr></table>')
112
- })
113
-
114
- it('handles cells with inline HTML (formatting tags)', () => {
115
- // The formatting-tag special case kicks in inside the cell content.
116
- const out = HtmlDiff.executeThreeWay(
117
- '<table><tr><td>The fee is due.</td></tr></table>',
118
- '<table><tr><td>The <strong>fee</strong> is due.</td></tr></table>',
119
- '<table><tr><td>The <strong>fee</strong> is due.</td></tr></table>'
120
- )
121
- expect(out).toContain("data-author='cp'")
122
- expect(out).toMatch(/<ins class='mod[^']*cp'/)
123
- expect(out).toContain('<table>')
124
- expect(out).toContain('</table>')
125
- })
126
-
127
- it('preserves attributes on the <table> element', () => {
104
+ it('rowspan attribute preserved with author attribution on cell content', () => {
128
105
  expect(
129
106
  HtmlDiff.executeThreeWay(
130
- '<table class="rates"><tr><td>Old</td></tr></table>',
131
- '<table class="rates"><tr><td>New</td></tr></table>',
132
- '<table class="rates"><tr><td>New</td></tr></table>'
107
+ '<table><tr><td rowspan="2">Foo</td></tr></table>',
108
+ '<table><tr><td rowspan="2">Bar</td></tr></table>',
109
+ '<table><tr><td rowspan="2">Foo</td></tr></table>'
133
110
  )
134
111
  ).toBe(
135
- "<table class=\"rates\"><tr><td><del class='diffdel cp' data-author='cp'>Old</del><ins class='diffins cp' data-author='cp'>New</ins></td></tr></table>"
112
+ "<table><tr><td rowspan=\"2\"><del class='diffdel cp' data-author='cp'>Foo</del><ins class='diffins cp' data-author='cp'>Bar</ins></td></tr></table>"
136
113
  )
137
114
  })
138
115
 
139
- it('multi-row table with edit in only one row', () => {
140
- // Cursor handoff between rows must work — row 0 unchanged, row 1 edited by CP, row 2 unchanged.
116
+ it('multi-row table with edit in one row only', () => {
141
117
  expect(
142
118
  HtmlDiff.executeThreeWay(
143
119
  '<table><tr><td>a</td></tr><tr><td>b</td></tr><tr><td>c</td></tr></table>',
144
120
  '<table><tr><td>a</td></tr><tr><td>B</td></tr><tr><td>c</td></tr></table>',
145
- '<table><tr><td>a</td></tr><tr><td>B</td></tr><tr><td>c</td></tr></table>'
121
+ '<table><tr><td>a</td></tr><tr><td>b</td></tr><tr><td>c</td></tr></table>'
146
122
  )
147
123
  ).toBe(
148
124
  "<table><tr><td>a</td></tr><tr><td><del class='diffdel cp' data-author='cp'>b</del><ins class='diffins cp' data-author='cp'>B</ins></td></tr><tr><td>c</td></tr></table>"
149
125
  )
150
126
  })
151
127
 
152
- it('preserves whitespace between row/cell tags', () => {
153
- // Real Word-sourced HTML has \n and indentation between tags;
154
- // confirm the cursor-based emission passes it through unchanged.
155
- const text = '<table>\n <tr>\n <td>a</td>\n </tr>\n</table>'
128
+ it('identity input passes through unchanged', () => {
129
+ const text = '<table><tr><td>a</td><td>b</td></tr><tr><td>c</td><td>d</td></tr></table>'
156
130
  expect(HtmlDiff.executeThreeWay(text, text, text)).toBe(text)
157
131
  })
158
132
  })
159
133
 
160
- describe('structural changes (rows added or removed)', () => {
161
- it('CP-inserted row (V1 V2 adds a row, Me kept it)', () => {
134
+ describe('structural row changes (rows added or removed)', () => {
135
+ it('CP inserted a row, Me kept the absence cp-attributed row insertion', () => {
162
136
  expect(
163
137
  HtmlDiff.executeThreeWay(
164
138
  '<table><tr><td>a</td></tr></table>',
165
139
  '<table><tr><td>a</td></tr><tr><td>b</td></tr></table>',
166
- '<table><tr><td>a</td></tr><tr><td>b</td></tr></table>'
140
+ '<table><tr><td>a</td></tr></table>'
167
141
  )
168
142
  ).toBe(
169
143
  "<table><tr><td>a</td></tr><tr class='diffins cp' data-author='cp'><td class='diffins cp' data-author='cp'><ins class='diffins cp' data-author='cp'>b</ins></td></tr></table>"
170
144
  )
171
145
  })
172
146
 
173
- it('Me-inserted row (V2 V3 adds a row, CP did nothing)', () => {
147
+ it('Me inserted a row, CP kept the absence me-attributed row insertion', () => {
174
148
  expect(
175
149
  HtmlDiff.executeThreeWay(
176
150
  '<table><tr><td>a</td></tr></table>',
@@ -182,19 +156,19 @@ describe('HtmlDiff.executeThreeWay (tables, positional case)', () => {
182
156
  )
183
157
  })
184
158
 
185
- it('CP-deleted V1 row (V1 V2 removes a row, Me kept the removal)', () => {
159
+ it('CP deleted a row, Me kept it cp-attributed row deletion (content from Me)', () => {
186
160
  expect(
187
161
  HtmlDiff.executeThreeWay(
188
162
  '<table><tr><td>a</td></tr><tr><td>b</td></tr></table>',
189
163
  '<table><tr><td>a</td></tr></table>',
190
- '<table><tr><td>a</td></tr></table>'
164
+ '<table><tr><td>a</td></tr><tr><td>b</td></tr></table>'
191
165
  )
192
166
  ).toBe(
193
167
  "<table><tr><td>a</td></tr><tr class='diffdel cp' data-author='cp'><td class='diffdel cp' data-author='cp'><del class='diffdel cp' data-author='cp'>b</del></td></tr></table>"
194
168
  )
195
169
  })
196
170
 
197
- it('Me-deleted V1 row (V2 == V1, Me removed it in V3)', () => {
171
+ it('Me deleted a row, CP kept it me-attributed row deletion', () => {
198
172
  expect(
199
173
  HtmlDiff.executeThreeWay(
200
174
  '<table><tr><td>a</td></tr><tr><td>b</td></tr></table>',
@@ -206,57 +180,31 @@ describe('HtmlDiff.executeThreeWay (tables, positional case)', () => {
206
180
  )
207
181
  })
208
182
 
209
- it('Me rejects CP-inserted row (CP added it, Me removed it)', () => {
183
+ it('Both inserted same row settled, no markup', () => {
210
184
  expect(
211
185
  HtmlDiff.executeThreeWay(
212
186
  '<table><tr><td>a</td></tr></table>',
213
187
  '<table><tr><td>a</td></tr><tr><td>b</td></tr></table>',
214
- '<table><tr><td>a</td></tr></table>'
215
- )
216
- ).toBe(
217
- "<table><tr><td>a</td></tr><tr class='diffdel me rejects-cp' data-author='me' data-rejects='cp'><td class='diffdel me rejects-cp' data-author='me' data-rejects='cp'><del class='diffdel me rejects-cp' data-author='me' data-rejects='cp'>b</del></td></tr></table>"
218
- )
219
- })
220
-
221
- it('both CP and Me add their own rows in different places', () => {
222
- expect(
223
- HtmlDiff.executeThreeWay(
224
- '<table><tr><td>row1</td></tr></table>',
225
- '<table><tr><td>row1</td></tr><tr><td>cp-added</td></tr></table>',
226
- '<table><tr><td>row1</td></tr><tr><td>cp-added</td></tr><tr><td>me-added</td></tr></table>'
188
+ '<table><tr><td>a</td></tr><tr><td>b</td></tr></table>'
227
189
  )
228
- ).toBe(
229
- "<table><tr><td>row1</td></tr><tr class='diffins cp' data-author='cp'><td class='diffins cp' data-author='cp'><ins class='diffins cp' data-author='cp'>cp-added</ins></td></tr><tr class='diffins me' data-author='me'><td class='diffins me' data-author='me'><ins class='diffins me' data-author='me'>me-added</ins></td></tr></table>"
230
- )
190
+ ).toBe('<table><tr><td>a</td></tr><tr><td>b</td></tr></table>')
231
191
  })
232
192
  })
233
193
 
234
- describe('multi-table mismatch (D3)', () => {
235
- it('CP added a whole new table, Me kept it', () => {
194
+ describe('multi-table (table count diverges)', () => {
195
+ it('CP added a whole new table, Me kept absence → cp insertion of whole table', () => {
236
196
  expect(
237
197
  HtmlDiff.executeThreeWay(
238
198
  '<table><tr><td>a</td></tr></table>',
239
199
  '<table><tr><td>a</td></tr></table><table><tr><td>b</td></tr></table>',
240
- '<table><tr><td>a</td></tr></table><table><tr><td>b</td></tr></table>'
241
- )
242
- ).toBe(
243
- "<table><tr><td>a</td></tr></table><ins class='diffins cp' data-author='cp'><table><tr><td>b</td></tr></table></ins>"
244
- )
245
- })
246
-
247
- it('CP removed a table that was in V1, Me kept it removed', () => {
248
- expect(
249
- HtmlDiff.executeThreeWay(
250
- '<table><tr><td>a</td></tr></table><table><tr><td>b</td></tr></table>',
251
- '<table><tr><td>a</td></tr></table>',
252
200
  '<table><tr><td>a</td></tr></table>'
253
201
  )
254
202
  ).toBe(
255
- "<table><tr><td>a</td></tr></table><del class='diffdel cp' data-author='cp'><table><tr><td>b</td></tr></table></del>"
203
+ "<table><tr><td>a</td></tr></table><ins class='diffins cp' data-author='cp'><table><tr><td>b</td></tr></table></ins>"
256
204
  )
257
205
  })
258
206
 
259
- it('Me added a brand-new table in V3 (CP did not touch it)', () => {
207
+ it('Me added a whole new table, CP kept absence me insertion', () => {
260
208
  expect(
261
209
  HtmlDiff.executeThreeWay(
262
210
  '<table><tr><td>a</td></tr></table>',
@@ -268,139 +216,85 @@ describe('HtmlDiff.executeThreeWay (tables, positional case)', () => {
268
216
  )
269
217
  })
270
218
 
271
- it('Me removed a table that V1 had and CP kept (Me-deletion of preserved table)', () => {
219
+ it('CP deleted a table from genesis, Me kept it cp deletion', () => {
272
220
  expect(
273
221
  HtmlDiff.executeThreeWay(
274
222
  '<table><tr><td>a</td></tr></table><table><tr><td>b</td></tr></table>',
275
- '<table><tr><td>a</td></tr></table><table><tr><td>b</td></tr></table>',
276
- '<table><tr><td>a</td></tr></table>'
223
+ '<table><tr><td>a</td></tr></table>',
224
+ '<table><tr><td>a</td></tr></table><table><tr><td>b</td></tr></table>'
277
225
  )
278
226
  ).toBe(
279
- "<table><tr><td>a</td></tr></table><del class='diffdel me' data-author='me'><table><tr><td>b</td></tr></table></del>"
227
+ "<table><tr><td>a</td></tr></table><del class='diffdel cp' data-author='cp'><table><tr><td>b</td></tr></table></del>"
280
228
  )
281
229
  })
282
230
 
283
- it('Me rejected a CP-added table (CP added, Me removed)', () => {
231
+ it('Me deleted a table from genesis, CP kept it → me deletion', () => {
284
232
  expect(
285
233
  HtmlDiff.executeThreeWay(
286
- '<table><tr><td>a</td></tr></table>',
234
+ '<table><tr><td>a</td></tr></table><table><tr><td>b</td></tr></table>',
287
235
  '<table><tr><td>a</td></tr></table><table><tr><td>b</td></tr></table>',
288
236
  '<table><tr><td>a</td></tr></table>'
289
237
  )
290
238
  ).toBe(
291
- "<table><tr><td>a</td></tr></table><del class='diffdel me rejects-cp' data-author='me' data-rejects='cp'><table><tr><td>b</td></tr></table></del>"
239
+ "<table><tr><td>a</td></tr></table><del class='diffdel me' data-author='me'><table><tr><td>b</td></tr></table></del>"
292
240
  )
293
241
  })
294
242
 
295
- it('falls through to content-LCS when tables are reordered (counts match but positions diverge)', () => {
296
- // V1: [Schedule of Fees, Penalty Notices]. V2 swaps them. V3 keeps the swap.
297
- // Counts match but the positional pairing would compare unrelated
298
- // tables — the similarity gate routes to content-LCS, which pairs
299
- // by content regardless of position.
300
- const fees = '<table><tr><td>Schedule of Fees</td><td>five percent annually</td></tr></table>'
301
- const penalties = '<table><tr><td>Penalty Notices</td><td>seven days written notice</td></tr></table>'
302
- const v1 = fees + penalties
303
- const v2 = penalties + fees
304
- const v3 = penalties + fees
305
- const out = HtmlDiff.executeThreeWay(v1, v2, v3)
306
- // Me did nothing — should produce zero Me attribution.
307
- expect(out).not.toContain("data-author='me'")
308
- // Both tables preserved verbatim (content-paired across positions).
309
- // Positional pairing would have produced wild ins/del replacing
310
- // "Schedule of Fees" with "Penalty Notices" and vice versa.
311
- expect(out).toContain('Schedule of Fees')
312
- expect(out).toContain('Penalty Notices')
313
- // Output should NOT contain replace-style ins/del wrapping the
314
- // distinctive table names (they should appear unchanged).
315
- expect(out).not.toMatch(/<del[^>]*>Schedule/)
316
- expect(out).not.toMatch(/<ins[^>]*>Penalty/)
317
- })
318
-
319
- it('both CP and Me each add their own new table', () => {
243
+ it('Both added the same new table settled, plain output', () => {
320
244
  expect(
321
245
  HtmlDiff.executeThreeWay(
322
246
  '<table><tr><td>orig</td></tr></table>',
323
- '<table><tr><td>orig</td></tr></table><table><tr><td>cp-added</td></tr></table>',
324
- '<table><tr><td>orig</td></tr></table><table><tr><td>cp-added</td></tr></table><table><tr><td>me-added</td></tr></table>'
247
+ '<table><tr><td>orig</td></tr></table><table><tr><td>added</td></tr></table>',
248
+ '<table><tr><td>orig</td></tr></table><table><tr><td>added</td></tr></table>'
325
249
  )
326
- ).toBe(
327
- "<table><tr><td>orig</td></tr></table><ins class='diffins cp' data-author='cp'><table><tr><td>cp-added</td></tr></table></ins><ins class='diffins me' data-author='me'><table><tr><td>me-added</td></tr></table></ins>"
328
- )
250
+ ).toBe('<table><tr><td>orig</td></tr></table><table><tr><td>added</td></tr></table>')
329
251
  })
330
252
  })
331
253
 
332
254
  describe('cell-level edge cases', () => {
333
- it('rowspan attribute preserved and content within attributed correctly', () => {
334
- // V1 has a rowspan="2" cell with "Foo"; CP edits to "Bar"; Me kept.
335
- // The rowspan attribute survives; the content is attributed.
336
- expect(
337
- HtmlDiff.executeThreeWay(
338
- '<table><tr><td rowspan="2">Foo</td></tr></table>',
339
- '<table><tr><td rowspan="2">Bar</td></tr></table>',
340
- '<table><tr><td rowspan="2">Bar</td></tr></table>'
341
- )
342
- ).toBe(
343
- "<table><tr><td rowspan=\"2\"><del class='diffdel cp' data-author='cp'>Foo</del><ins class='diffins cp' data-author='cp'>Bar</ins></td></tr></table>"
344
- )
345
- })
346
-
347
255
  it('exceedsSizeLimit bail-out: oversized table falls through to word-level path', () => {
348
- // Build a table with > 1500 rows. preprocessTablesThreeWay returns
349
- // null on size cap, so executeThreeWay treats the table as raw HTML
350
- // — output is V2 (since V1==V2==V3 in this construction) verbatim.
256
+ // 1501-row table: preprocessTablesThreeWay returns null on size cap,
257
+ // so executeThreeWay treats the table as raw HTML.
351
258
  const rows = Array.from({ length: 1501 }, (_, i) => `<tr><td>row ${i}</td></tr>`).join('')
352
259
  const html = `<table>${rows}</table>`
353
- // Identical inputs — output is verbatim regardless of size-cap path.
354
260
  expect(HtmlDiff.executeThreeWay(html, html, html)).toBe(html)
355
261
  })
356
262
  })
357
263
 
358
264
  describe('nested tables', () => {
359
265
  it('handles a table nested inside a cell — both attributed correctly', () => {
360
- // Outer table with a single cell containing an inner table. CP edits
361
- // the inner table's content; Me doesn't touch it. The recursion
362
- // through cellDiff should reach the inner table and attribute it.
363
- const v1 = '<table><tr><td><table><tr><td>inner</td></tr></table></td></tr></table>'
364
- const v2 = '<table><tr><td><table><tr><td>INNER</td></tr></table></td></tr></table>'
365
- const v3 = '<table><tr><td><table><tr><td>INNER</td></tr></table></td></tr></table>'
366
- const out = HtmlDiff.executeThreeWay(v1, v2, v3)
367
- // Inner cell content attributed to CP via the recursive cellDiff.
266
+ const out = HtmlDiff.executeThreeWay(
267
+ '<table><tr><td><table><tr><td>inner</td></tr></table></td></tr></table>',
268
+ '<table><tr><td><table><tr><td>INNER</td></tr></table></td></tr></table>',
269
+ '<table><tr><td><table><tr><td>inner</td></tr></table></td></tr></table>'
270
+ )
368
271
  expect(out).toMatch(/<del[^>]*data-author='cp'[^>]*>inner<\/del>/)
369
272
  expect(out).toMatch(/<ins[^>]*data-author='cp'[^>]*>INNER<\/ins>/)
370
- // Outer table scaffolding intact.
371
273
  expect(out.startsWith('<table><tr><td><table>')).toBe(true)
372
274
  expect(out.endsWith('</table></td></tr></table>')).toBe(true)
373
275
  })
374
276
 
375
- it('safely handles deeply-nested tables without infinite recursion', () => {
376
- // Build a 10-deep nested table. The MaxThreeWayDepth cap kicks in
377
- // at level 8, falling back to word-level treatment of deeper tables.
378
- // The test just verifies no crash and reasonable output.
277
+ it('deeply-nested identity tables pass through unchanged (depth cap exercised)', () => {
379
278
  let v = 'leaf'
380
279
  for (let i = 0; i < 10; i++) v = `<table><tr><td>${v}</td></tr></table>`
381
- // Identical inputs — should produce input verbatim regardless of depth.
382
280
  expect(HtmlDiff.executeThreeWay(v, v, v)).toBe(v)
383
281
  })
384
282
  })
385
283
 
386
284
  describe('integration with surrounding prose', () => {
387
- it('attributes both the cell content AND the surrounding prose', () => {
388
- const out = HtmlDiff.executeThreeWay(
389
- '<p>Rates as follows:</p><table><tr><td>five</td></tr></table>',
390
- '<p>Interest rates as follows:</p><table><tr><td>five and a half</td></tr></table>',
391
- '<p>Interest rates as follows:</p><table><tr><td>five</td></tr></table>'
285
+ it('prose and table both attributed correctly', () => {
286
+ expect(
287
+ HtmlDiff.executeThreeWay(
288
+ '<p>Rates as follows:</p><table><tr><td>five</td></tr></table>',
289
+ '<p>Interest rates as follows:</p><table><tr><td>five and a half</td></tr></table>',
290
+ '<p>Rates as follows:</p><table><tr><td>five</td></tr></table>'
291
+ )
292
+ ).toBe(
293
+ "<p><del class='diffdel cp' data-author='cp'>Rates</del><ins class='diffins cp' data-author='cp'>Interest rates</ins> as follows:</p><table><tr><td>five<ins class='diffins cp' data-author='cp'>&nbsp;and a half</ins></td></tr></table>"
392
294
  )
393
- // CP edited the prose (added "Interest "), Me rejected CP's table edit.
394
- expect(out).toContain("<ins class='diffins cp' data-author='cp'>Interest")
395
- expect(out).toContain("data-rejects='cp'")
396
- // Table scaffolding intact.
397
- expect(out).toContain('<table>')
398
- expect(out).toContain('</table>')
399
295
  })
400
296
 
401
- it('passes inputs with no tables through to the word-level path unchanged', () => {
402
- // No tables → preprocessTablesThreeWay returns null → executeThreeWay
403
- // skips straight to word-level. Sanity check the no-op.
297
+ it('no-tables path is unaffected', () => {
404
298
  expect(HtmlDiff.executeThreeWay('<p>a</p>', '<p>a</p>', '<p>a</p>')).toBe('<p>a</p>')
405
299
  })
406
300
  })