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