@createiq/htmldiff 1.2.0-beta.0 → 1.2.0-beta.2
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/README.md +46 -19
- package/dist/HtmlDiff.cjs +418 -420
- package/dist/HtmlDiff.cjs.map +1 -1
- package/dist/HtmlDiff.d.cts +30 -1
- package/dist/HtmlDiff.d.mts +30 -1
- package/dist/HtmlDiff.mjs +418 -420
- package/dist/HtmlDiff.mjs.map +1 -1
- package/package.json +1 -1
- package/src/HtmlDiff.ts +106 -50
- package/src/ThreeWayDiff.ts +173 -127
- package/src/ThreeWayTable.ts +408 -484
- package/test/HtmlDiff.spec.ts +15 -0
- package/test/HtmlDiff.threeWay.spec.ts +117 -108
- package/test/HtmlDiff.threeWay.tables.spec.ts +88 -194
|
@@ -2,67 +2,70 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
import HtmlDiff from '../src/HtmlDiff'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
11
|
-
'<table><tr><td>Interest Rate</td
|
|
12
|
-
'<table><tr><td>
|
|
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
|
|
26
|
+
it('Me only changed, CP kept genesis → me attribution', () => {
|
|
20
27
|
expect(
|
|
21
28
|
HtmlDiff.executeThreeWay(
|
|
22
|
-
'<table><tr><td>Rate</td
|
|
23
|
-
'<table><tr><td>Rate</td
|
|
24
|
-
'<table><tr><td>Rate
|
|
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'> percent</ins></td></tr></table>"
|
|
28
|
-
)
|
|
33
|
+
).toBe("<table><tr><td>Rate<ins class='diffins me' data-author='me'> per annum</ins></td></tr></table>")
|
|
29
34
|
})
|
|
30
35
|
|
|
31
|
-
it('
|
|
36
|
+
it('Settled — both made the same change → no markup', () => {
|
|
32
37
|
expect(
|
|
33
38
|
HtmlDiff.executeThreeWay(
|
|
34
|
-
'<table><tr><td>Rate</td
|
|
35
|
-
'<table><tr><td>Interest Rate</td
|
|
36
|
-
'<table><tr><td>Interest Rate</td
|
|
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'> percent</ins></td></tr></table>"
|
|
40
|
-
)
|
|
43
|
+
).toBe('<table><tr><td>Interest Rate</td></tr></table>')
|
|
41
44
|
})
|
|
42
45
|
|
|
43
|
-
it('
|
|
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>
|
|
51
|
+
'<table><tr><td>seven</td></tr></table>'
|
|
49
52
|
)
|
|
50
53
|
).toBe(
|
|
51
|
-
"<table><tr><td
|
|
54
|
+
"<table><tr><td><del class='diffdel me' data-author='me'>five</del><ins class='diffins cp' data-author='cp'> 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
|
|
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'> and a half</del></td></tr></table>")
|
|
63
66
|
})
|
|
64
67
|
|
|
65
|
-
it('Me
|
|
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('
|
|
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>
|
|
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('
|
|
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>
|
|
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('
|
|
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
|
|
131
|
-
'<table
|
|
132
|
-
'<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
|
|
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
|
|
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>
|
|
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('
|
|
153
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
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
|
|
235
|
-
it('CP added a whole new table, Me kept
|
|
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><
|
|
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
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
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>
|
|
324
|
-
'<table><tr><td>orig</td></tr></table><table><tr><td>
|
|
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
|
-
//
|
|
349
|
-
//
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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('
|
|
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('
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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'> 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('
|
|
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
|
})
|