UNPKG

16.1 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2018 TypeFox and others.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License v. 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is available at
12// https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16
17import { expect } from 'chai';
18import { MockTreeModel } from './test/mock-tree-model';
19import { createTreeTestContainer } from './test/tree-test-container';
20import { TreeModel } from './tree-model';
21import { SelectableTreeNode, TreeSelection } from './tree-selection';
22import { TreeSelectionState } from './tree-selection-state';
23
24namespace TreeSelectionState {
25
26 export interface Expectation {
27 readonly focus?: string | undefined;
28 readonly selection?: string[];
29 }
30
31 export interface Assert {
32 readonly nextState: (type: 'default' | 'toggle' | 'range', nodeId: string, expectation?: Expectation) => Assert;
33 }
34
35}
36
37const LARGE_FLAT_MOCK_ROOT = (length = 250000) => {
38 const children = Array.from({ length }, (_, idx) => ({ 'id': (idx + 1).toString() }));
39 return MockTreeModel.Node.toTreeNode({
40 'id': 'ROOT',
41 'children': [
42 ...children
43 ]
44 });
45};
46
47describe('tree-selection-state', () => {
48
49 const model = createTreeModel();
50 const findNode = (nodeId: string) => model.getNode(nodeId) as (SelectableTreeNode);
51
52 beforeEach(() => {
53 model.root = MockTreeModel.FLAT_MOCK_ROOT();
54 expect(model.selectedNodes).to.be.empty;
55 });
56
57 it('should remove the selection but not the focus when toggling the one and only selected node', () => {
58 newState()
59 .nextState('toggle', '1', {
60 focus: '1',
61 selection: ['1']
62 })
63 .nextState('toggle', '1', {
64 focus: '1',
65 selection: []
66 });
67 });
68
69 it('should keep the focus on the `fromNode` when selecting a range', () => {
70 newState()
71 .nextState('toggle', '1')
72 .nextState('range', '3', {
73 focus: '1',
74 selection: ['1', '2', '3']
75 });
76 });
77
78 it('should always have one single focus node when toggling node in a range', () => {
79 newState()
80 .nextState('toggle', '1')
81 .nextState('range', '3')
82 .nextState('toggle', '2', {
83 focus: '1',
84 selection: ['1', '3']
85 })
86 .nextState('toggle', '2', {
87 focus: '1',
88 selection: ['1', '2', '3']
89 });
90 });
91
92 it('should calculate the range from the focus even unselecting a node in the previously created range', () => {
93 newState()
94 .nextState('toggle', '5')
95 .nextState('range', '2', {
96 focus: '5',
97 selection: ['2', '3', '4', '5']
98 })
99 .nextState('toggle', '3', {
100 focus: '5',
101 selection: ['2', '4', '5']
102 })
103 .nextState('range', '7', {
104 focus: '5',
105 selection: ['2', '4', '5', '6', '7']
106 });
107 });
108
109 it('should discard the previous range selection state if the current one has the same focus with other direction', () => {
110 newState()
111 .nextState('toggle', '4')
112 .nextState('range', '1')
113 .nextState('range', '6', {
114 focus: '4',
115 selection: ['4', '5', '6']
116 });
117 });
118
119 it('should discard the previous range selection state if the current one overlaps the previous one', () => {
120 newState()
121 .nextState('toggle', '4')
122 .nextState('range', '6')
123 .nextState('range', '8', {
124 focus: '4',
125 selection: ['4', '5', '6', '7', '8']
126 });
127 });
128
129 it('should move the focus to the most recently selected node if the previous selection was a toggle', () => {
130 newState()
131 .nextState('toggle', '4')
132 .nextState('toggle', '5', {
133 focus: '5',
134 selection: ['4', '5']
135 });
136 });
137
138 it('should keep the focus on the previous node if the current toggling happens in the most recent range', () => {
139 newState()
140 .nextState('toggle', '4')
141 .nextState('range', '2', {
142 focus: '4',
143 selection: ['4', '3', '2']
144 });
145 });
146
147 it('should move the focus to the next toggle node if that is out of the latest range selection', () => {
148 newState()
149 .nextState('toggle', '3')
150 .nextState('range', '5', {
151 focus: '3',
152 selection: ['3', '4', '5']
153 })
154 .nextState('toggle', '1', {
155 focus: '1',
156 selection: ['3', '4', '5', '1']
157 });
158 });
159
160 it('should not change the focus when removing a selection from an individual node', () => {
161 newState()
162 .nextState('toggle', '3')
163 .nextState('range', '5', {
164 focus: '3',
165 selection: ['5', '4', '3']
166 })
167 .nextState('toggle', '1', {
168 focus: '1',
169 selection: ['1', '5', '4', '3']
170 })
171 .nextState('toggle', '4', {
172 focus: '1',
173 selection: ['1', '5', '3']
174 });
175 });
176
177 it('should discard the previously selected range if each node from the previous range is contained in the current one', () => {
178 newState()
179 .nextState('toggle', '5')
180 .nextState('range', '2')
181 .nextState('toggle', '3', {
182 focus: '5',
183 selection: ['5', '4', '2']
184 })
185 .nextState('range', '1', {
186 focus: '5',
187 selection: ['5', '4', '3', '2', '1']
188 })
189 .nextState('range', '7', {
190 focus: '5',
191 selection: ['5', '6', '7']
192 });
193 });
194
195 it('should be possible to remove the selection of a node which is contained in two overlapping ranges', () => {
196 newState()
197 .nextState('toggle', '5')
198 .nextState('range', '4')
199 .nextState('toggle', '3')
200 .nextState('range', '2', {
201 focus: '3',
202 selection: ['2', '3', '4', '5']
203 })
204 .nextState('range', '5', {
205 focus: '3',
206 selection: ['5', '4', '3']
207 })
208 .nextState('toggle', '4', {
209 focus: '3',
210 selection: ['5', '3']
211 });
212 });
213
214 it('should be possible to traverse with range selections', () => {
215 newState()
216 .nextState('toggle', '2')
217 .nextState('range', '3', {
218 focus: '2', // In VSCode this is 3.
219 selection: ['2', '3']
220 })
221 .nextState('range', '4', {
222 focus: '2', // In VSCode this is 4. They distinguish between `Shift + Up Arrow` and `Shift + Click`.
223 selection: ['2', '3', '4']
224 });
225 });
226
227 it('should remove the selection from a node inside a range instead of setting the focus', () => {
228 newState()
229 .nextState('toggle', '2')
230 .nextState('range', '3', {
231 focus: '2',
232 selection: ['2', '3']
233 })
234 .nextState('toggle', '5', {
235 focus: '5',
236 selection: ['2', '3', '5']
237 })
238 .nextState('range', '6', {
239 focus: '5',
240 selection: ['2', '3', '5', '6']
241 })
242 .nextState('toggle', '3', {
243 focus: '5',
244 selection: ['2', '5', '6']
245 });
246 });
247
248 it('should merge all individual selections into a range', () => {
249 newState()
250 .nextState('toggle', '1')
251 .nextState('toggle', '3')
252 .nextState('toggle', '6')
253 .nextState('toggle', '2')
254 .nextState('range', '7', {
255 focus: '2',
256 selection: ['1', '2', '3', '4', '5', '6', '7']
257 });
258 });
259
260 it('should keep focus on the most recent even when removing selection from individual nodes', () => {
261 newState()
262 .nextState('toggle', '9')
263 .nextState('range', '6', {
264 focus: '9',
265 selection: ['9', '8', '7', '6']
266 })
267 .nextState('range', '10', {
268 focus: '9',
269 selection: ['9', '10']
270 })
271 .nextState('range', '2', {
272 focus: '9',
273 selection: ['9', '8', '7', '6', '5', '4', '3', '2']
274 })
275 .nextState('toggle', '4', {
276 focus: '9',
277 selection: ['9', '8', '7', '6', '5', '3', '2']
278 })
279 .nextState('toggle', '5', {
280 focus: '9',
281 selection: ['9', '8', '7', '6', '3', '2']
282 })
283 .nextState('toggle', '6', {
284 focus: '9',
285 selection: ['9', '8', '7', '3', '2']
286 })
287 .nextState('toggle', '7', {
288 focus: '9',
289 selection: ['9', '8', '3', '2']
290 })
291 .nextState('range', '5', {
292 focus: '9',
293 selection: ['9', '8', '7', '6', '5', '3', '2']
294 });
295 });
296
297 it('should expand the range instead of discarding it if individual elements have created a hole in the range', () => {
298 newState()
299 .nextState('toggle', '9')
300 .nextState('range', '3', {
301 focus: '9',
302 selection: ['9', '8', '7', '6', '5', '4', '3']
303 })
304 .nextState('toggle', '4', {
305 focus: '9',
306 selection: ['9', '8', '7', '6', '5', '3']
307 })
308 .nextState('range', '10', {
309 focus: '9',
310 selection: ['10', '9', '8', '7', '6', '5', '3']
311 });
312 });
313
314 it('should reset the selection state and the focus when using the default selection', () => {
315 newState()
316 .nextState('toggle', '1')
317 .nextState('toggle', '2', {
318 focus: '2',
319 selection: ['1', '2']
320 })
321 .nextState('default', '2', {
322 focus: '2',
323 selection: ['2']
324 })
325 .nextState('toggle', '1', {
326 focus: '1',
327 selection: ['1', '2']
328 })
329 .nextState('default', '2', {
330 focus: '2',
331 selection: ['2']
332 });
333 });
334
335 it('should remove the selection but keep the focus when toggling the single selected node', () => {
336 newState()
337 .nextState('toggle', '1', {
338 focus: '1',
339 selection: ['1']
340 })
341 .nextState('toggle', '1', {
342 focus: '1',
343 selection: []
344 });
345 });
346
347 it('should treat ranges with the same from and to node as a single element range', () => {
348 newState()
349 .nextState('toggle', '2')
350 .nextState('range', '1', {
351 focus: '2',
352 selection: ['1', '2']
353 })
354 .nextState('range', '2', {
355 focus: '2',
356 selection: ['2']
357 })
358 .nextState('range', '3', {
359 focus: '2',
360 selection: ['2', '3']
361 })
362 .nextState('range', '2', {
363 focus: '2',
364 selection: ['2']
365 })
366 .nextState('range', '1', {
367 focus: '2',
368 selection: ['1', '2']
369 });
370 });
371
372 it('should remember the most recent range selection start after toggling individual nodes', () => {
373 newState()
374 .nextState('toggle', '10', {
375 focus: '10',
376 selection: ['10']
377 })
378 .nextState('range', '3', {
379 focus: '10',
380 selection: ['3', '4', '5', '6', '7', '8', '9', '10']
381 })
382 .nextState('toggle', '7', {
383 focus: '10',
384 selection: ['3', '4', '5', '6', '8', '9', '10']
385 })
386 .nextState('toggle', '8', {
387 focus: '10',
388 selection: ['3', '4', '5', '6', '9', '10']
389 })
390 .nextState('toggle', '6', {
391 focus: '10',
392 selection: ['3', '4', '5', '9', '10']
393 })
394 .nextState('toggle', '5', {
395 focus: '10',
396 selection: ['3', '4', '9', '10']
397 })
398 .nextState('range', '2', {
399 focus: '10',
400 selection: ['2', '3', '4', '5', '6', '7', '8', '9', '10']
401 });
402 });
403
404 it('should be able to handle range selection on large tree', () => {
405 model.root = LARGE_FLAT_MOCK_ROOT();
406 expect(model.selectedNodes).to.be.empty;
407
408 const start = 10;
409 const end = 20;
410 newState()
411 .nextState('toggle', start.toString(), {
412 focus: start.toString(),
413 selection: [start.toString()]
414 })
415 .nextState('range', end.toString(), {
416 focus: start.toString(),
417 selection: Array.from({ length: end - start + 1 }, (_, idx) => (start + idx).toString())
418 });
419 });
420
421 function newState(): TreeSelectionState.Assert {
422 return nextState(new TreeSelectionState(model));
423 }
424
425 function nextState(state: TreeSelectionState): TreeSelectionState.Assert {
426 return {
427 nextState: (nextType, nextId, expectation) => {
428 const node = findNode(nextId);
429 const type = ((t: 'default' | 'toggle' | 'range') => {
430 switch (t) {
431 case 'default': return TreeSelection.SelectionType.DEFAULT;
432 case 'toggle': return TreeSelection.SelectionType.TOGGLE;
433 case 'range': return TreeSelection.SelectionType.RANGE;
434 default: throw new Error(`Unexpected selection type: ${t}.`);
435 }
436 })(nextType);
437 const next = state.nextState({ node, type });
438 if (!!expectation) {
439 const { focus, selection } = expectation;
440 if ('focus' in expectation) {
441 if (focus === undefined) {
442 expect(next.focus).to.be.undefined;
443 } else {
444 expect(next.focus).to.be.not.undefined;
445 expect(next.focus!.id).to.be.equal(focus);
446 // TODO: we need tree-selection tests too, otherwise, we cannot verify whether there is one focus or not.
447 }
448 }
449 if (selection) {
450 expect(next.selection().map(n => n.id).sort()).to.be.deep.equal(selection.sort());
451 }
452 }
453 return nextState(next);
454 }
455 };
456 }
457
458 function createTreeModel(): TreeModel {
459 return createTreeTestContainer().get(TreeModel);
460 }
461
462});
463
\No newline at end of file