| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | */ |
| 7 | |
| 8 | import {ChunkSelectState} from '../chunkSelectState'; |
| 9 | |
| 10 | describe('ChunkSelectState', () => { |
| 11 | const a = 'aa\nbb\ncc\ndd\n'; |
| 12 | const b = 'cc\ndd\nee\nff\n'; |
| 13 | |
| 14 | describe('fromText()', () => { |
| 15 | it('with none selected', () => { |
| 16 | const state = ChunkSelectState.fromText(a, b, false); |
| 17 | expect(renderLines(state)).toMatchObject([ |
| 18 | '[ ] - 1 aa', |
| 19 | '[ ] - 2 bb', |
| 20 | ' 3 1 cc', |
| 21 | ' 4 2 dd', |
| 22 | '[ ] + 3 ee', |
| 23 | '[ ] + 4 ff', |
| 24 | ]); |
| 25 | expect(state.getSelectedText()).toBe(a); |
| 26 | }); |
| 27 | |
| 28 | it('with all changes selected', () => { |
| 29 | const state = ChunkSelectState.fromText(a, b, true); |
| 30 | expect(renderLines(state)).toMatchObject([ |
| 31 | '[x] - 1 aa', |
| 32 | '[x] - 2 bb', |
| 33 | ' 3 1 cc', |
| 34 | ' 4 2 dd', |
| 35 | '[x] + 3 ee', |
| 36 | '[x] + 4 ff', |
| 37 | ]); |
| 38 | expect(state.getSelectedText()).toBe(b); |
| 39 | }); |
| 40 | |
| 41 | it('with free-form partial selection', () => { |
| 42 | const text = 'aa\ncc\ndd\nee\n'; |
| 43 | const state = ChunkSelectState.fromText(a, b, text); |
| 44 | expect(renderLines(state)).toMatchObject([ |
| 45 | '[ ] - 1 aa', |
| 46 | '[x] - 2 bb', |
| 47 | ' 3 1 cc', |
| 48 | ' 4 2 dd', |
| 49 | '[x] + 3 ee', |
| 50 | '[ ] + 4 ff', |
| 51 | ]); |
| 52 | expect(state.getSelectedText()).toBe(text); |
| 53 | }); |
| 54 | |
| 55 | it('with free-form extra deletions', () => { |
| 56 | const text = ''; |
| 57 | const state = ChunkSelectState.fromText(a, b, text); |
| 58 | expect(renderLines(state)).toMatchObject([ |
| 59 | '[x] - 1 aa', |
| 60 | '[x] - 2 bb', |
| 61 | ' !- 3 1 cc', |
| 62 | ' !- 4 2 dd', |
| 63 | '[ ] + 3 ee', |
| 64 | '[ ] + 4 ff', |
| 65 | ]); |
| 66 | expect(state.getSelectedText()).toBe(text); |
| 67 | }); |
| 68 | |
| 69 | it('with free-form extra insertions', () => { |
| 70 | const text = 'aa\ncc\n<insertion>\ndd\nee\n'; |
| 71 | const state = ChunkSelectState.fromText(a, b, text); |
| 72 | expect(renderLines(state)).toMatchObject([ |
| 73 | '[ ] - 1 aa', |
| 74 | '[x] - 2 bb', |
| 75 | ' 3 1 cc', |
| 76 | ' !+ <insertion>', |
| 77 | ' 4 2 dd', |
| 78 | '[x] + 3 ee', |
| 79 | '[ ] + 4 ff', |
| 80 | ]); |
| 81 | expect(state.getSelectedText()).toBe(text); |
| 82 | }); |
| 83 | |
| 84 | it('sorts changes, deletion is before insertion', () => { |
| 85 | const state = ChunkSelectState.fromText('aa\naa\n', 'bb\nbb\nbb\n', true); |
| 86 | expect(renderLines(state)).toMatchObject([ |
| 87 | '[x] - 1 aa', |
| 88 | '[x] - 2 aa', |
| 89 | '[x] + 1 bb', |
| 90 | '[x] + 2 bb', |
| 91 | '[x] + 3 bb', |
| 92 | ]); |
| 93 | }); |
| 94 | }); |
| 95 | |
| 96 | describe('setSelectedLines()', () => { |
| 97 | it('toggles deleted lines', () => { |
| 98 | let state = ChunkSelectState.fromText(a, b, false); |
| 99 | state = state.setSelectedLines([ |
| 100 | [0, true], |
| 101 | [1, false], |
| 102 | ]); |
| 103 | expect(renderLines(state).slice(0, 2)).toMatchObject(['[x] - 1 aa', '[ ] - 2 bb']); |
| 104 | state = state.setSelectedLines([ |
| 105 | [0, false], |
| 106 | [1, true], |
| 107 | ]); |
| 108 | expect(renderLines(state).slice(0, 2)).toMatchObject(['[ ] - 1 aa', '[x] - 2 bb']); |
| 109 | }); |
| 110 | |
| 111 | it('toggles added lines', () => { |
| 112 | let state = ChunkSelectState.fromText(a, b, false); |
| 113 | state = state.setSelectedLines([ |
| 114 | [4, true], |
| 115 | [5, false], |
| 116 | ]); |
| 117 | expect(renderLines(state).slice(4, 6)).toMatchObject(['[x] + 3 ee', '[ ] + 4 ff']); |
| 118 | state = state.setSelectedLines([ |
| 119 | [4, false], |
| 120 | [5, true], |
| 121 | ]); |
| 122 | expect(renderLines(state).slice(4, 6)).toMatchObject(['[ ] + 3 ee', '[x] + 4 ff']); |
| 123 | }); |
| 124 | |
| 125 | it('does nothing to other lines', () => { |
| 126 | const text = 'aa\ncc\n<insertion>\ndd\nee\n'; |
| 127 | let state = ChunkSelectState.fromText(a, b, text); |
| 128 | state = state.setSelectedLines([ |
| 129 | [2, false], |
| 130 | [3, false], |
| 131 | [4, true], |
| 132 | ]); |
| 133 | expect(renderLines(state)).toMatchObject([ |
| 134 | '[ ] - 1 aa', |
| 135 | '[x] - 2 bb', |
| 136 | ' 3 1 cc', |
| 137 | ' !+ <insertion>', |
| 138 | ' 4 2 dd', |
| 139 | '[x] + 3 ee', |
| 140 | '[ ] + 4 ff', |
| 141 | ]); |
| 142 | expect(state.getSelectedText()).toBe(text); |
| 143 | }); |
| 144 | }); |
| 145 | |
| 146 | describe('setSelectedText()', () => { |
| 147 | it('round-trips with getSelectedText()', () => { |
| 148 | let state = ChunkSelectState.fromText(a, b, false); |
| 149 | const lines = ['aa\n', 'bb\n', 'cc\n', 'ii\n', 'dd\n', 'ee\n', 'ff\n']; |
| 150 | // eslint-disable-next-line no-bitwise |
| 151 | const end = 1 << lines.length; |
| 152 | for (let bits = 0; bits < end; ++bits) { |
| 153 | // eslint-disable-next-line no-bitwise |
| 154 | const text = lines.map((l, i) => ((bits & (1 << i)) === 0 ? '' : l)).join(''); |
| 155 | state = state.setSelectedText(text); |
| 156 | expect(state.getSelectedText()).toBe(text); |
| 157 | } |
| 158 | }); |
| 159 | }); |
| 160 | |
| 161 | describe('getInverseText()', () => { |
| 162 | it('produces changes with inverse selection', () => { |
| 163 | const state = ChunkSelectState.fromText(a, b, a).setSelectedLines([ |
| 164 | [0, true], |
| 165 | [4, true], |
| 166 | ]); |
| 167 | expect(renderLines(state)).toMatchObject([ |
| 168 | '[x] - 1 aa', |
| 169 | '[ ] - 2 bb', |
| 170 | ' 3 1 cc', |
| 171 | ' 4 2 dd', |
| 172 | '[x] + 3 ee', |
| 173 | '[ ] + 4 ff', |
| 174 | ]); |
| 175 | expect(state.getSelectedText()).toBe('bb\ncc\ndd\nee\n'); |
| 176 | expect(state.getInverseText()).toBe('aa\ncc\ndd\nff\n'); |
| 177 | }); |
| 178 | }); |
| 179 | |
| 180 | describe('getLineRegions()', () => { |
| 181 | it('produces a region when nothing is changed', () => { |
| 182 | const state = ChunkSelectState.fromText(a, a, a); |
| 183 | expect(state.getLineRegions()).toMatchObject([ |
| 184 | { |
| 185 | lines: state.getLines(), |
| 186 | same: true, |
| 187 | collapsed: true, |
| 188 | }, |
| 189 | ]); |
| 190 | }); |
| 191 | |
| 192 | it('produces a region when everything is changed', () => { |
| 193 | const a = 'a\na\na\n'; |
| 194 | const b = 'b\nb\nb\nb\n'; |
| 195 | [a, b].forEach(m => { |
| 196 | const state = ChunkSelectState.fromText(a, b, m); |
| 197 | expect(state.getLineRegions()).toMatchObject([ |
| 198 | { |
| 199 | lines: state.getLines(), |
| 200 | same: false, |
| 201 | collapsed: false, |
| 202 | }, |
| 203 | ]); |
| 204 | }); |
| 205 | }); |
| 206 | |
| 207 | it('produces regions with complex changes', () => { |
| 208 | const state = ChunkSelectState.fromText( |
| 209 | '1\n2\n3\n4\n8\n9\n', |
| 210 | '1\n2\n3\n5\n8\n9\n', |
| 211 | '1\n2\n3\n5\n8\n9\n', |
| 212 | ); |
| 213 | const lines = state.getLines(); |
| 214 | expect(state.getLineRegions()).toMatchObject([ |
| 215 | { |
| 216 | lines: lines.slice(0, 1), |
| 217 | collapsed: true, |
| 218 | same: true, |
| 219 | }, |
| 220 | { |
| 221 | lines: lines.slice(1, 3), |
| 222 | collapsed: false, |
| 223 | same: true, |
| 224 | }, |
| 225 | { |
| 226 | lines: lines.slice(3, 5), |
| 227 | collapsed: false, |
| 228 | same: false, |
| 229 | }, |
| 230 | { |
| 231 | lines: lines.slice(5, 7), |
| 232 | collapsed: false, |
| 233 | same: true, |
| 234 | }, |
| 235 | ]); |
| 236 | }); |
| 237 | }); |
| 238 | }); |
| 239 | |
| 240 | /** Visualize line selections in ASCII. */ |
| 241 | function renderLines(state: ChunkSelectState): string[] { |
| 242 | return state.getLines().map(l => { |
| 243 | const checkbox = {true: '[x]', false: '[ ]', null: ' '}[`${l.selected}`]; |
| 244 | const aLine = l.aLine === null ? '' : l.aLine.toString(); |
| 245 | const bLine = l.bLine === null ? '' : l.bLine.toString(); |
| 246 | return [ |
| 247 | checkbox.padStart(3), |
| 248 | l.sign.padStart(2), |
| 249 | aLine.padStart(3), |
| 250 | bLine.padStart(3), |
| 251 | ' ', |
| 252 | l.data.trimEnd(), |
| 253 | ].join(''); |
| 254 | }); |
| 255 | } |
| 256 | |