refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {devModeEqual} from '@angular/core/src/change_detection/change_detection_util';
|
||||
import {describe, expect, it} from '@angular/core/testing/testing_internal';
|
||||
|
||||
export function main() {
|
||||
describe('ChangeDetectionUtil', () => {
|
||||
describe('devModeEqual', () => {
|
||||
it('should do the deep comparison of iterables', () => {
|
||||
expect(devModeEqual([['one']], [['one']])).toBe(true);
|
||||
expect(devModeEqual(['one'], ['one', 'two'])).toBe(false);
|
||||
expect(devModeEqual(['one', 'two'], ['one'])).toBe(false);
|
||||
expect(devModeEqual(['one'], 'one')).toBe(false);
|
||||
expect(devModeEqual(['one'], new Object())).toBe(false);
|
||||
expect(devModeEqual('one', ['one'])).toBe(false);
|
||||
expect(devModeEqual(new Object(), ['one'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should compare primitive numbers', () => {
|
||||
expect(devModeEqual(1, 1)).toBe(true);
|
||||
expect(devModeEqual(1, 2)).toBe(false);
|
||||
expect(devModeEqual(new Object(), 2)).toBe(false);
|
||||
expect(devModeEqual(1, new Object())).toBe(false);
|
||||
});
|
||||
|
||||
it('should compare primitive strings', () => {
|
||||
expect(devModeEqual('one', 'one')).toBe(true);
|
||||
expect(devModeEqual('one', 'two')).toBe(false);
|
||||
expect(devModeEqual(new Object(), 'one')).toBe(false);
|
||||
expect(devModeEqual('one', new Object())).toBe(false);
|
||||
});
|
||||
|
||||
it('should compare primitive booleans', () => {
|
||||
expect(devModeEqual(true, true)).toBe(true);
|
||||
expect(devModeEqual(true, false)).toBe(false);
|
||||
expect(devModeEqual(new Object(), true)).toBe(false);
|
||||
expect(devModeEqual(true, new Object())).toBe(false);
|
||||
});
|
||||
|
||||
it('should compare null', () => {
|
||||
expect(devModeEqual(null, null)).toBe(true);
|
||||
expect(devModeEqual(null, 1)).toBe(false);
|
||||
expect(devModeEqual(new Object(), null)).toBe(false);
|
||||
expect(devModeEqual(null, new Object())).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for other objects',
|
||||
() => { expect(devModeEqual(new Object(), new Object())).toBe(true); });
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,592 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DefaultIterableDiffer, DefaultIterableDifferFactory} from '@angular/core/src/change_detection/differs/default_iterable_differ';
|
||||
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
|
||||
|
||||
import {TestIterable} from '../../change_detection/iterable';
|
||||
import {iterableChangesAsString} from '../../change_detection/util';
|
||||
|
||||
class ItemWithId {
|
||||
constructor(private id: string) {}
|
||||
|
||||
toString() { return `{id: ${this.id}}`; }
|
||||
}
|
||||
|
||||
class ComplexItem {
|
||||
constructor(private id: string, private color: string) {}
|
||||
|
||||
toString() { return `{id: ${this.id}, color: ${this.color}}`; }
|
||||
}
|
||||
|
||||
// todo(vicb): UnmodifiableListView / frozen object when implemented
|
||||
export function main() {
|
||||
describe('iterable differ', function() {
|
||||
describe('DefaultIterableDiffer', function() {
|
||||
let differ: any /** TODO #9100 */;
|
||||
|
||||
beforeEach(() => { differ = new DefaultIterableDiffer(); });
|
||||
|
||||
it('should support list and iterables', () => {
|
||||
const f = new DefaultIterableDifferFactory();
|
||||
expect(f.supports([])).toBeTruthy();
|
||||
expect(f.supports(new TestIterable())).toBeTruthy();
|
||||
expect(f.supports(new Map())).toBeFalsy();
|
||||
expect(f.supports(null)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should support iterables', () => {
|
||||
const l = new TestIterable();
|
||||
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({collection: []}));
|
||||
|
||||
l.list = [1];
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['1[null->0]'],
|
||||
additions: ['1[null->0]']
|
||||
}));
|
||||
|
||||
l.list = [2, 1];
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['2[null->0]', '1[0->1]'],
|
||||
previous: ['1[0->1]'],
|
||||
additions: ['2[null->0]'],
|
||||
moves: ['1[0->1]']
|
||||
}));
|
||||
});
|
||||
|
||||
it('should detect additions', () => {
|
||||
const l: any[] /** TODO #9100 */ = [];
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({collection: []}));
|
||||
|
||||
l.push('a');
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['a[null->0]'],
|
||||
additions: ['a[null->0]']
|
||||
}));
|
||||
|
||||
l.push('b');
|
||||
differ.check(l);
|
||||
expect(differ.toString())
|
||||
.toEqual(iterableChangesAsString(
|
||||
{collection: ['a', 'b[null->1]'], previous: ['a'], additions: ['b[null->1]']}));
|
||||
});
|
||||
|
||||
it('should support changing the reference', () => {
|
||||
let l = [0];
|
||||
differ.check(l);
|
||||
|
||||
l = [1, 0];
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['1[null->0]', '0[0->1]'],
|
||||
previous: ['0[0->1]'],
|
||||
additions: ['1[null->0]'],
|
||||
moves: ['0[0->1]']
|
||||
}));
|
||||
|
||||
l = [2, 1, 0];
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['2[null->0]', '1[0->1]', '0[1->2]'],
|
||||
previous: ['1[0->1]', '0[1->2]'],
|
||||
additions: ['2[null->0]'],
|
||||
moves: ['1[0->1]', '0[1->2]']
|
||||
}));
|
||||
});
|
||||
|
||||
it('should handle swapping element', () => {
|
||||
const l = [1, 2];
|
||||
differ.check(l);
|
||||
|
||||
l.length = 0;
|
||||
l.push(2);
|
||||
l.push(1);
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['2[1->0]', '1[0->1]'],
|
||||
previous: ['1[0->1]', '2[1->0]'],
|
||||
moves: ['2[1->0]', '1[0->1]']
|
||||
}));
|
||||
});
|
||||
|
||||
it('should handle incremental swapping element', () => {
|
||||
const l = ['a', 'b', 'c'];
|
||||
differ.check(l);
|
||||
|
||||
l.splice(1, 1);
|
||||
l.splice(0, 0, 'b');
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['b[1->0]', 'a[0->1]', 'c'],
|
||||
previous: ['a[0->1]', 'b[1->0]', 'c'],
|
||||
moves: ['b[1->0]', 'a[0->1]']
|
||||
}));
|
||||
|
||||
l.splice(1, 1);
|
||||
l.push('a');
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['b', 'c[2->1]', 'a[1->2]'],
|
||||
previous: ['b', 'a[1->2]', 'c[2->1]'],
|
||||
moves: ['c[2->1]', 'a[1->2]']
|
||||
}));
|
||||
});
|
||||
|
||||
it('should detect changes in list', () => {
|
||||
const l: any[] /** TODO #9100 */ = [];
|
||||
differ.check(l);
|
||||
|
||||
l.push('a');
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['a[null->0]'],
|
||||
additions: ['a[null->0]']
|
||||
}));
|
||||
|
||||
l.push('b');
|
||||
differ.check(l);
|
||||
expect(differ.toString())
|
||||
.toEqual(iterableChangesAsString(
|
||||
{collection: ['a', 'b[null->1]'], previous: ['a'], additions: ['b[null->1]']}));
|
||||
|
||||
l.push('c');
|
||||
l.push('d');
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['a', 'b', 'c[null->2]', 'd[null->3]'],
|
||||
previous: ['a', 'b'],
|
||||
additions: ['c[null->2]', 'd[null->3]']
|
||||
}));
|
||||
|
||||
l.splice(2, 1);
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['a', 'b', 'd[3->2]'],
|
||||
previous: ['a', 'b', 'c[2->null]', 'd[3->2]'],
|
||||
moves: ['d[3->2]'],
|
||||
removals: ['c[2->null]']
|
||||
}));
|
||||
|
||||
l.length = 0;
|
||||
l.push('d');
|
||||
l.push('c');
|
||||
l.push('b');
|
||||
l.push('a');
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['d[2->0]', 'c[null->1]', 'b[1->2]', 'a[0->3]'],
|
||||
previous: ['a[0->3]', 'b[1->2]', 'd[2->0]'],
|
||||
additions: ['c[null->1]'],
|
||||
moves: ['d[2->0]', 'b[1->2]', 'a[0->3]']
|
||||
}));
|
||||
});
|
||||
|
||||
it('should test string by value rather than by reference (Dart)', () => {
|
||||
const l = ['a', 'boo'];
|
||||
differ.check(l);
|
||||
|
||||
const b = 'b';
|
||||
const oo = 'oo';
|
||||
l[1] = b + oo;
|
||||
differ.check(l);
|
||||
expect(differ.toString())
|
||||
.toEqual(iterableChangesAsString({collection: ['a', 'boo'], previous: ['a', 'boo']}));
|
||||
});
|
||||
|
||||
it('should ignore [NaN] != [NaN] (JS)', () => {
|
||||
const l = [NaN];
|
||||
differ.check(l);
|
||||
differ.check(l);
|
||||
expect(differ.toString())
|
||||
.toEqual(iterableChangesAsString({collection: [NaN], previous: [NaN]}));
|
||||
});
|
||||
|
||||
it('should detect [NaN] moves', () => {
|
||||
const l: any[] = [NaN, NaN];
|
||||
differ.check(l);
|
||||
|
||||
l.unshift('foo');
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['foo[null->0]', 'NaN[0->1]', 'NaN[1->2]'],
|
||||
previous: ['NaN[0->1]', 'NaN[1->2]'],
|
||||
additions: ['foo[null->0]'],
|
||||
moves: ['NaN[0->1]', 'NaN[1->2]']
|
||||
}));
|
||||
});
|
||||
|
||||
it('should remove and add same item', () => {
|
||||
const l = ['a', 'b', 'c'];
|
||||
differ.check(l);
|
||||
|
||||
l.splice(1, 1);
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['a', 'c[2->1]'],
|
||||
previous: ['a', 'b[1->null]', 'c[2->1]'],
|
||||
moves: ['c[2->1]'],
|
||||
removals: ['b[1->null]']
|
||||
}));
|
||||
|
||||
l.splice(1, 0, 'b');
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['a', 'b[null->1]', 'c[1->2]'],
|
||||
previous: ['a', 'c[1->2]'],
|
||||
additions: ['b[null->1]'],
|
||||
moves: ['c[1->2]']
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should support duplicates', () => {
|
||||
const l = ['a', 'a', 'a', 'b', 'b'];
|
||||
differ.check(l);
|
||||
|
||||
l.splice(0, 1);
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['a', 'a', 'b[3->2]', 'b[4->3]'],
|
||||
previous: ['a', 'a', 'a[2->null]', 'b[3->2]', 'b[4->3]'],
|
||||
moves: ['b[3->2]', 'b[4->3]'],
|
||||
removals: ['a[2->null]']
|
||||
}));
|
||||
});
|
||||
|
||||
it('should support insertions/moves', () => {
|
||||
const l = ['a', 'a', 'b', 'b'];
|
||||
differ.check(l);
|
||||
|
||||
l.splice(0, 0, 'b');
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['b[2->0]', 'a[0->1]', 'a[1->2]', 'b', 'b[null->4]'],
|
||||
previous: ['a[0->1]', 'a[1->2]', 'b[2->0]', 'b'],
|
||||
additions: ['b[null->4]'],
|
||||
moves: ['b[2->0]', 'a[0->1]', 'a[1->2]']
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not report unnecessary moves', () => {
|
||||
const l = ['a', 'b', 'c'];
|
||||
differ.check(l);
|
||||
|
||||
l.length = 0;
|
||||
l.push('b');
|
||||
l.push('a');
|
||||
l.push('c');
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['b[1->0]', 'a[0->1]', 'c'],
|
||||
previous: ['a[0->1]', 'b[1->0]', 'c'],
|
||||
moves: ['b[1->0]', 'a[0->1]']
|
||||
}));
|
||||
});
|
||||
|
||||
describe('forEachOperation', () => {
|
||||
function stringifyItemChange(record: any, p: number, c: number, originalIndex: number) {
|
||||
const suffix = originalIndex == null ? '' : ' [o=' + originalIndex + ']';
|
||||
const value = record.item;
|
||||
if (record.currentIndex == null) {
|
||||
return `REMOVE ${value} (${p} -> VOID)${suffix}`;
|
||||
} else if (record.previousIndex == null) {
|
||||
return `INSERT ${value} (VOID -> ${c})${suffix}`;
|
||||
} else {
|
||||
return `MOVE ${value} (${p} -> ${c})${suffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
function modifyArrayUsingOperation(
|
||||
arr: number[], endData: any[], prev: number, next: number) {
|
||||
let value: number = null;
|
||||
if (prev == null) {
|
||||
value = endData[next];
|
||||
arr.splice(next, 0, value);
|
||||
} else if (next == null) {
|
||||
value = arr[prev];
|
||||
arr.splice(prev, 1);
|
||||
} else {
|
||||
value = arr[prev];
|
||||
arr.splice(prev, 1);
|
||||
arr.splice(next, 0, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
it('should trigger a series of insert/move/remove changes for inputs that have been diffed',
|
||||
() => {
|
||||
const startData = [0, 1, 2, 3, 4, 5];
|
||||
const endData = [6, 2, 7, 0, 4, 8];
|
||||
|
||||
differ = differ.diff(startData);
|
||||
differ = differ.diff(endData);
|
||||
|
||||
const operations: string[] = [];
|
||||
differ.forEachOperation((item: any, prev: number, next: number) => {
|
||||
const value = modifyArrayUsingOperation(startData, endData, prev, next);
|
||||
operations.push(stringifyItemChange(item, prev, next, item.previousIndex));
|
||||
});
|
||||
|
||||
expect(operations).toEqual([
|
||||
'INSERT 6 (VOID -> 0)', 'MOVE 2 (3 -> 1) [o=2]', 'INSERT 7 (VOID -> 2)',
|
||||
'REMOVE 1 (4 -> VOID) [o=1]', 'REMOVE 3 (4 -> VOID) [o=3]',
|
||||
'REMOVE 5 (5 -> VOID) [o=5]', 'INSERT 8 (VOID -> 5)'
|
||||
]);
|
||||
|
||||
expect(startData).toEqual(endData);
|
||||
});
|
||||
|
||||
it('should consider inserting/removing/moving items with respect to items that have not moved at all',
|
||||
() => {
|
||||
const startData = [0, 1, 2, 3];
|
||||
const endData = [2, 1];
|
||||
|
||||
differ = differ.diff(startData);
|
||||
differ = differ.diff(endData);
|
||||
|
||||
const operations: string[] = [];
|
||||
differ.forEachOperation((item: any, prev: number, next: number) => {
|
||||
const value = modifyArrayUsingOperation(startData, endData, prev, next);
|
||||
operations.push(stringifyItemChange(item, prev, next, item.previousIndex));
|
||||
});
|
||||
|
||||
expect(operations).toEqual([
|
||||
'REMOVE 0 (0 -> VOID) [o=0]', 'MOVE 2 (1 -> 0) [o=2]', 'REMOVE 3 (2 -> VOID) [o=3]'
|
||||
]);
|
||||
|
||||
expect(startData).toEqual(endData);
|
||||
});
|
||||
|
||||
it('should be able to manage operations within a criss/cross of move operations', () => {
|
||||
const startData = [1, 2, 3, 4, 5, 6];
|
||||
const endData = [3, 6, 4, 9, 1, 2];
|
||||
|
||||
differ = differ.diff(startData);
|
||||
differ = differ.diff(endData);
|
||||
|
||||
const operations: string[] = [];
|
||||
differ.forEachOperation((item: any, prev: number, next: number) => {
|
||||
const value = modifyArrayUsingOperation(startData, endData, prev, next);
|
||||
operations.push(stringifyItemChange(item, prev, next, item.previousIndex));
|
||||
});
|
||||
|
||||
expect(operations).toEqual([
|
||||
'MOVE 3 (2 -> 0) [o=2]', 'MOVE 6 (5 -> 1) [o=5]', 'MOVE 4 (4 -> 2) [o=3]',
|
||||
'INSERT 9 (VOID -> 3)', 'REMOVE 5 (6 -> VOID) [o=4]'
|
||||
]);
|
||||
|
||||
expect(startData).toEqual(endData);
|
||||
});
|
||||
|
||||
it('should skip moves for multiple nodes that have not moved', () => {
|
||||
const startData = [0, 1, 2, 3, 4];
|
||||
const endData = [4, 1, 2, 3, 0, 5];
|
||||
|
||||
differ = differ.diff(startData);
|
||||
differ = differ.diff(endData);
|
||||
|
||||
const operations: string[] = [];
|
||||
differ.forEachOperation((item: any, prev: number, next: number) => {
|
||||
const value = modifyArrayUsingOperation(startData, endData, prev, next);
|
||||
operations.push(stringifyItemChange(item, prev, next, item.previousIndex));
|
||||
});
|
||||
|
||||
expect(operations).toEqual([
|
||||
'MOVE 4 (4 -> 0) [o=4]', 'MOVE 1 (2 -> 1) [o=1]', 'MOVE 2 (3 -> 2) [o=2]',
|
||||
'MOVE 3 (4 -> 3) [o=3]', 'INSERT 5 (VOID -> 5)'
|
||||
]);
|
||||
|
||||
expect(startData).toEqual(endData);
|
||||
});
|
||||
|
||||
it('should not fail', () => {
|
||||
const startData = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||
const endData = [10, 11, 1, 5, 7, 8, 0, 5, 3, 6];
|
||||
|
||||
differ = differ.diff(startData);
|
||||
differ = differ.diff(endData);
|
||||
|
||||
const operations: string[] = [];
|
||||
differ.forEachOperation((item: any, prev: number, next: number) => {
|
||||
const value = modifyArrayUsingOperation(startData, endData, prev, next);
|
||||
operations.push(stringifyItemChange(item, prev, next, item.previousIndex));
|
||||
});
|
||||
|
||||
expect(operations).toEqual([
|
||||
'MOVE 10 (10 -> 0) [o=10]', 'MOVE 11 (11 -> 1) [o=11]', 'MOVE 1 (3 -> 2) [o=1]',
|
||||
'MOVE 5 (7 -> 3) [o=5]', 'MOVE 7 (9 -> 4) [o=7]', 'MOVE 8 (10 -> 5) [o=8]',
|
||||
'REMOVE 2 (7 -> VOID) [o=2]', 'INSERT 5 (VOID -> 7)', 'REMOVE 4 (9 -> VOID) [o=4]',
|
||||
'REMOVE 9 (10 -> VOID) [o=9]'
|
||||
]);
|
||||
|
||||
expect(startData).toEqual(endData);
|
||||
});
|
||||
|
||||
it('should trigger nothing when the list is completely full of replaced items that are tracked by the index',
|
||||
() => {
|
||||
differ = new DefaultIterableDiffer((index: number) => index);
|
||||
|
||||
const startData = [1, 2, 3, 4];
|
||||
const endData = [5, 6, 7, 8];
|
||||
|
||||
differ = differ.diff(startData);
|
||||
differ = differ.diff(endData);
|
||||
|
||||
const operations: string[] = [];
|
||||
differ.forEachOperation((item: any, prev: number, next: number) => {
|
||||
const value = modifyArrayUsingOperation(startData, endData, prev, next);
|
||||
operations.push(stringifyItemChange(item, prev, next, item.previousIndex));
|
||||
});
|
||||
|
||||
expect(operations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('diff', () => {
|
||||
it('should return self when there is a change', () => {
|
||||
expect(differ.diff(['a', 'b'])).toBe(differ);
|
||||
});
|
||||
|
||||
it('should return null when there is no change', () => {
|
||||
differ.diff(['a', 'b']);
|
||||
expect(differ.diff(['a', 'b'])).toEqual(null);
|
||||
});
|
||||
|
||||
it('should treat null as an empty list', () => {
|
||||
differ.diff(['a', 'b']);
|
||||
expect(differ.diff(null).toString()).toEqual(iterableChangesAsString({
|
||||
previous: ['a[0->null]', 'b[1->null]'],
|
||||
removals: ['a[0->null]', 'b[1->null]']
|
||||
}));
|
||||
});
|
||||
|
||||
it('should throw when given an invalid collection', () => {
|
||||
expect(() => differ.diff('invalid')).toThrowError('Error trying to diff \'invalid\'');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackBy function by id', function() {
|
||||
let differ: any /** TODO #9100 */;
|
||||
|
||||
const trackByItemId = (index: number, item: any): any => item.id;
|
||||
|
||||
const buildItemList = (list: string[]) => list.map((val) => new ItemWithId(val));
|
||||
|
||||
beforeEach(() => { differ = new DefaultIterableDiffer(trackByItemId); });
|
||||
|
||||
it('should treat the collection as dirty if identity changes', () => {
|
||||
differ.diff(buildItemList(['a']));
|
||||
expect(differ.diff(buildItemList(['a']))).toBe(differ);
|
||||
});
|
||||
|
||||
it('should treat seen records as identity changes, not additions', () => {
|
||||
let l = buildItemList(['a', 'b', 'c']);
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: [`{id: a}[null->0]`, `{id: b}[null->1]`, `{id: c}[null->2]`],
|
||||
additions: [`{id: a}[null->0]`, `{id: b}[null->1]`, `{id: c}[null->2]`]
|
||||
}));
|
||||
|
||||
l = buildItemList(['a', 'b', 'c']);
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: [`{id: a}`, `{id: b}`, `{id: c}`],
|
||||
identityChanges: [`{id: a}`, `{id: b}`, `{id: c}`],
|
||||
previous: [`{id: a}`, `{id: b}`, `{id: c}`]
|
||||
}));
|
||||
});
|
||||
|
||||
it('should have updated properties in identity change collection', () => {
|
||||
let l = [new ComplexItem('a', 'blue'), new ComplexItem('b', 'yellow')];
|
||||
differ.check(l);
|
||||
|
||||
l = [new ComplexItem('a', 'orange'), new ComplexItem('b', 'red')];
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: [`{id: a, color: orange}`, `{id: b, color: red}`],
|
||||
identityChanges: [`{id: a, color: orange}`, `{id: b, color: red}`],
|
||||
previous: [`{id: a, color: orange}`, `{id: b, color: red}`]
|
||||
}));
|
||||
});
|
||||
|
||||
it('should track moves normally', () => {
|
||||
let l = buildItemList(['a', 'b', 'c']);
|
||||
differ.check(l);
|
||||
|
||||
l = buildItemList(['b', 'a', 'c']);
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['{id: b}[1->0]', '{id: a}[0->1]', '{id: c}'],
|
||||
identityChanges: ['{id: b}[1->0]', '{id: a}[0->1]', '{id: c}'],
|
||||
previous: ['{id: a}[0->1]', '{id: b}[1->0]', '{id: c}'],
|
||||
moves: ['{id: b}[1->0]', '{id: a}[0->1]']
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
it('should track duplicate reinsertion normally', () => {
|
||||
let l = buildItemList(['a', 'a']);
|
||||
differ.check(l);
|
||||
|
||||
l = buildItemList(['b', 'a', 'a']);
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['{id: b}[null->0]', '{id: a}[0->1]', '{id: a}[1->2]'],
|
||||
identityChanges: ['{id: a}[0->1]', '{id: a}[1->2]'],
|
||||
previous: ['{id: a}[0->1]', '{id: a}[1->2]'],
|
||||
moves: ['{id: a}[0->1]', '{id: a}[1->2]'],
|
||||
additions: ['{id: b}[null->0]']
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
it('should track removals normally', () => {
|
||||
const l = buildItemList(['a', 'b', 'c']);
|
||||
differ.check(l);
|
||||
|
||||
l.splice(2, 1);
|
||||
differ.check(l);
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['{id: a}', '{id: b}'],
|
||||
previous: ['{id: a}', '{id: b}', '{id: c}[2->null]'],
|
||||
removals: ['{id: c}[2->null]']
|
||||
}));
|
||||
});
|
||||
});
|
||||
describe('trackBy function by index', function() {
|
||||
let differ: any /** TODO #9100 */;
|
||||
|
||||
const trackByIndex = (index: number, item: any): number => index;
|
||||
|
||||
beforeEach(() => { differ = new DefaultIterableDiffer(trackByIndex); });
|
||||
|
||||
it('should track removals normally', () => {
|
||||
differ.check(['a', 'b', 'c', 'd']);
|
||||
differ.check(['e', 'f', 'g', 'h']);
|
||||
differ.check(['e', 'f', 'h']);
|
||||
|
||||
expect(differ.toString()).toEqual(iterableChangesAsString({
|
||||
collection: ['e', 'f', 'h'],
|
||||
previous: ['e', 'f', 'h', 'h[3->null]'],
|
||||
removals: ['h[3->null]'],
|
||||
identityChanges: ['h']
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
@ -0,0 +1,229 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DefaultKeyValueDiffer, DefaultKeyValueDifferFactory} from '@angular/core/src/change_detection/differs/default_keyvalue_differ';
|
||||
import {afterEach, beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
|
||||
import {kvChangesAsString} from '../../change_detection/util';
|
||||
|
||||
// todo(vicb): Update the code & tests for object equality
|
||||
export function main() {
|
||||
describe('keyvalue differ', function() {
|
||||
describe('DefaultKeyValueDiffer', function() {
|
||||
let differ: DefaultKeyValueDiffer<any, any>;
|
||||
let m: Map<any, any>;
|
||||
|
||||
beforeEach(() => {
|
||||
differ = new DefaultKeyValueDiffer<string, any>();
|
||||
m = new Map();
|
||||
});
|
||||
|
||||
afterEach(() => { differ = null; });
|
||||
|
||||
it('should detect additions', () => {
|
||||
differ.check(m);
|
||||
|
||||
m.set('a', 1);
|
||||
differ.check(m);
|
||||
expect(differ.toString())
|
||||
.toEqual(kvChangesAsString({map: ['a[null->1]'], additions: ['a[null->1]']}));
|
||||
|
||||
m.set('b', 2);
|
||||
differ.check(m);
|
||||
expect(differ.toString())
|
||||
.toEqual(kvChangesAsString(
|
||||
{map: ['a', 'b[null->2]'], previous: ['a'], additions: ['b[null->2]']}));
|
||||
});
|
||||
|
||||
it('should handle changing key/values correctly', () => {
|
||||
m.set(1, 10);
|
||||
m.set(2, 20);
|
||||
differ.check(m);
|
||||
|
||||
m.set(2, 10);
|
||||
m.set(1, 20);
|
||||
differ.check(m);
|
||||
expect(differ.toString()).toEqual(kvChangesAsString({
|
||||
map: ['1[10->20]', '2[20->10]'],
|
||||
previous: ['1[10->20]', '2[20->10]'],
|
||||
changes: ['1[10->20]', '2[20->10]']
|
||||
}));
|
||||
});
|
||||
|
||||
it('should expose previous and current value', () => {
|
||||
let previous: any /** TODO #9100 */, current: any /** TODO #9100 */;
|
||||
|
||||
m.set(1, 10);
|
||||
differ.check(m);
|
||||
|
||||
m.set(1, 20);
|
||||
differ.check(m);
|
||||
|
||||
differ.forEachChangedItem((record: any /** TODO #9100 */) => {
|
||||
previous = record.previousValue;
|
||||
current = record.currentValue;
|
||||
});
|
||||
|
||||
expect(previous).toEqual(10);
|
||||
expect(current).toEqual(20);
|
||||
});
|
||||
|
||||
it('should do basic map watching', () => {
|
||||
differ.check(m);
|
||||
|
||||
m.set('a', 'A');
|
||||
differ.check(m);
|
||||
expect(differ.toString())
|
||||
.toEqual(kvChangesAsString({map: ['a[null->A]'], additions: ['a[null->A]']}));
|
||||
|
||||
m.set('b', 'B');
|
||||
differ.check(m);
|
||||
expect(differ.toString())
|
||||
.toEqual(kvChangesAsString(
|
||||
{map: ['a', 'b[null->B]'], previous: ['a'], additions: ['b[null->B]']}));
|
||||
|
||||
m.set('b', 'BB');
|
||||
m.set('d', 'D');
|
||||
differ.check(m);
|
||||
expect(differ.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a', 'b[B->BB]', 'd[null->D]'],
|
||||
previous: ['a', 'b[B->BB]'],
|
||||
additions: ['d[null->D]'],
|
||||
changes: ['b[B->BB]']
|
||||
}));
|
||||
|
||||
m.delete('b');
|
||||
differ.check(m);
|
||||
expect(differ.toString())
|
||||
.toEqual(kvChangesAsString(
|
||||
{map: ['a', 'd'], previous: ['a', 'b[BB->null]', 'd'], removals: ['b[BB->null]']}));
|
||||
|
||||
m.clear();
|
||||
differ.check(m);
|
||||
expect(differ.toString()).toEqual(kvChangesAsString({
|
||||
previous: ['a[A->null]', 'd[D->null]'],
|
||||
removals: ['a[A->null]', 'd[D->null]']
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not see a NaN value as a change', () => {
|
||||
m.set('foo', Number.NaN);
|
||||
differ.check(m);
|
||||
|
||||
differ.check(m);
|
||||
expect(differ.toString()).toEqual(kvChangesAsString({map: ['foo'], previous: ['foo']}));
|
||||
});
|
||||
|
||||
it('should work regardless key order', () => {
|
||||
m.set('a', 0);
|
||||
m.set('b', 0);
|
||||
differ.check(m);
|
||||
|
||||
m = new Map();
|
||||
m.set('b', 1);
|
||||
m.set('a', 1);
|
||||
differ.check(m);
|
||||
|
||||
expect(differ.toString()).toEqual(kvChangesAsString({
|
||||
map: ['b[0->1]', 'a[0->1]'],
|
||||
previous: ['a[0->1]', 'b[0->1]'],
|
||||
changes: ['b[0->1]', 'a[0->1]']
|
||||
}));
|
||||
});
|
||||
|
||||
describe('JsObject changes', () => {
|
||||
it('should support JS Object', () => {
|
||||
const f = new DefaultKeyValueDifferFactory();
|
||||
expect(f.supports({})).toBeTruthy();
|
||||
expect(f.supports('not supported')).toBeFalsy();
|
||||
expect(f.supports(0)).toBeFalsy();
|
||||
expect(f.supports(null)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should do basic object watching', () => {
|
||||
let m: {[k: string]: string} = {};
|
||||
differ.check(m);
|
||||
|
||||
m['a'] = 'A';
|
||||
differ.check(m);
|
||||
expect(differ.toString())
|
||||
.toEqual(kvChangesAsString({map: ['a[null->A]'], additions: ['a[null->A]']}));
|
||||
|
||||
m['b'] = 'B';
|
||||
differ.check(m);
|
||||
expect(differ.toString())
|
||||
.toEqual(kvChangesAsString(
|
||||
{map: ['a', 'b[null->B]'], previous: ['a'], additions: ['b[null->B]']}));
|
||||
|
||||
m['b'] = 'BB';
|
||||
m['d'] = 'D';
|
||||
differ.check(m);
|
||||
expect(differ.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a', 'b[B->BB]', 'd[null->D]'],
|
||||
previous: ['a', 'b[B->BB]'],
|
||||
additions: ['d[null->D]'],
|
||||
changes: ['b[B->BB]']
|
||||
}));
|
||||
|
||||
m = {};
|
||||
m['a'] = 'A';
|
||||
m['d'] = 'D';
|
||||
differ.check(m);
|
||||
expect(differ.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a', 'd'],
|
||||
previous: ['a', 'b[BB->null]', 'd'],
|
||||
removals: ['b[BB->null]']
|
||||
}));
|
||||
|
||||
m = {};
|
||||
differ.check(m);
|
||||
expect(differ.toString()).toEqual(kvChangesAsString({
|
||||
previous: ['a[A->null]', 'd[D->null]'],
|
||||
removals: ['a[A->null]', 'd[D->null]']
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
it('should work regardless key order', () => {
|
||||
differ.check({a: 0, b: 0});
|
||||
differ.check({b: 1, a: 1});
|
||||
|
||||
expect(differ.toString()).toEqual(kvChangesAsString({
|
||||
map: ['b[0->1]', 'a[0->1]'],
|
||||
previous: ['a[0->1]', 'b[0->1]'],
|
||||
changes: ['b[0->1]', 'a[0->1]']
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('diff', () => {
|
||||
it('should return self when there is a change', () => {
|
||||
m.set('a', 'A');
|
||||
expect(differ.diff(m)).toBe(differ);
|
||||
});
|
||||
|
||||
it('should return null when there is no change', () => {
|
||||
m.set('a', 'A');
|
||||
differ.diff(m);
|
||||
expect(differ.diff(m)).toEqual(null);
|
||||
});
|
||||
|
||||
it('should treat null as an empty list', () => {
|
||||
m.set('a', 'A');
|
||||
differ.diff(m);
|
||||
expect(differ.diff(null).toString())
|
||||
.toEqual(kvChangesAsString({previous: ['a[A->null]'], removals: ['a[A->null]']}));
|
||||
});
|
||||
|
||||
it('should throw when given an invalid collection', () => {
|
||||
expect(() => differ.diff(<any>'invalid'))
|
||||
.toThrowError('Error trying to diff \'invalid\'');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ReflectiveInjector} from '@angular/core';
|
||||
import {IterableDiffers} from '@angular/core/src/change_detection/differs/iterable_differs';
|
||||
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
|
||||
|
||||
import {SpyIterableDifferFactory} from '../../spies';
|
||||
|
||||
export function main() {
|
||||
describe('IterableDiffers', function() {
|
||||
let factory1: any /** TODO #9100 */;
|
||||
let factory2: any /** TODO #9100 */;
|
||||
let factory3: any /** TODO #9100 */;
|
||||
|
||||
beforeEach(() => {
|
||||
factory1 = new SpyIterableDifferFactory();
|
||||
factory2 = new SpyIterableDifferFactory();
|
||||
factory3 = new SpyIterableDifferFactory();
|
||||
});
|
||||
|
||||
it('should throw when no suitable implementation found', () => {
|
||||
const differs = new IterableDiffers([]);
|
||||
expect(() => differs.find('some object'))
|
||||
.toThrowError(/Cannot find a differ supporting object 'some object'/);
|
||||
});
|
||||
|
||||
it('should return the first suitable implementation', () => {
|
||||
factory1.spy('supports').and.returnValue(false);
|
||||
factory2.spy('supports').and.returnValue(true);
|
||||
factory3.spy('supports').and.returnValue(true);
|
||||
|
||||
const differs = IterableDiffers.create(<any>[factory1, factory2, factory3]);
|
||||
expect(differs.find('some object')).toBe(factory2);
|
||||
});
|
||||
|
||||
it('should copy over differs from the parent repo', () => {
|
||||
factory1.spy('supports').and.returnValue(true);
|
||||
factory2.spy('supports').and.returnValue(false);
|
||||
|
||||
const parent = IterableDiffers.create(<any>[factory1]);
|
||||
const child = IterableDiffers.create(<any>[factory2], parent);
|
||||
|
||||
expect(child.factories).toEqual([factory2, factory1]);
|
||||
});
|
||||
|
||||
describe('.extend()', () => {
|
||||
it('should throw if calling extend when creating root injector', () => {
|
||||
const injector = ReflectiveInjector.resolveAndCreate([IterableDiffers.extend([])]);
|
||||
|
||||
expect(() => injector.get(IterableDiffers))
|
||||
.toThrowError(/Cannot extend IterableDiffers without a parent injector/);
|
||||
});
|
||||
|
||||
it('should extend di-inherited diffesr', () => {
|
||||
const parent = new IterableDiffers([factory1]);
|
||||
const injector =
|
||||
ReflectiveInjector.resolveAndCreate([{provide: IterableDiffers, useValue: parent}]);
|
||||
const childInjector = injector.resolveAndCreateChild([IterableDiffers.extend([factory2])]);
|
||||
|
||||
expect(injector.get(IterableDiffers).factories).toEqual([factory1]);
|
||||
expect(childInjector.get(IterableDiffers).factories).toEqual([factory2, factory1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
16
packages/core/test/change_detection/iterable.ts
Normal file
16
packages/core/test/change_detection/iterable.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {getSymbolIterator} from '@angular/core/src/util';
|
||||
|
||||
export class TestIterable {
|
||||
list: number[];
|
||||
constructor() { this.list = []; }
|
||||
|
||||
[getSymbolIterator()]() { return (this.list as any)[getSymbolIterator()](); }
|
||||
}
|
37
packages/core/test/change_detection/util.ts
Normal file
37
packages/core/test/change_detection/util.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export function iterableChangesAsString(
|
||||
{collection = [] as any, previous = [] as any, additions = [] as any, moves = [] as any,
|
||||
removals = [] as any, identityChanges = [] as any}): string {
|
||||
return 'collection: ' + collection.join(', ') + '\n' +
|
||||
'previous: ' + previous.join(', ') + '\n' +
|
||||
'additions: ' + additions.join(', ') + '\n' +
|
||||
'moves: ' + moves.join(', ') + '\n' +
|
||||
'removals: ' + removals.join(', ') + '\n' +
|
||||
'identityChanges: ' + identityChanges.join(', ') + '\n';
|
||||
}
|
||||
|
||||
export function kvChangesAsString(
|
||||
{map, previous, additions, changes, removals}:
|
||||
{map?: any[], previous?: any[], additions?: any[], changes?: any[], removals?: any[]}):
|
||||
string {
|
||||
if (!map) map = [];
|
||||
if (!previous) previous = [];
|
||||
if (!additions) additions = [];
|
||||
if (!changes) changes = [];
|
||||
if (!removals) removals = [];
|
||||
|
||||
return 'map: ' + map.join(', ') + '\n' +
|
||||
'previous: ' + previous.join(', ') + '\n' +
|
||||
'additions: ' + additions.join(', ') + '\n' +
|
||||
'changes: ' + changes.join(', ') + '\n' +
|
||||
'removals: ' + removals.join(', ') + '\n';
|
||||
}
|
Reference in New Issue
Block a user