up to the
125 | // current position and diff it. The
![]()
will then be deleted.
126 | expect(container.children[0]!.innerHTML).to.equal('Hola');
127 | });
128 | });
--------------------------------------------------------------------------------
/test/functional/styles_spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
2 | /** @license SPDX-License-Identifier: Apache-2.0 */
3 |
4 | // taze: mocha from //third_party/javascript/typings/mocha
5 | // taze: chai from //third_party/javascript/typings/chai
6 | import {elementVoid, patch} from '../../index';
7 | const {expect} = chai;
8 |
9 | describe('style updates', () => {
10 | let container;
11 |
12 | beforeEach(() => {
13 | container = document.createElement('div');
14 | document.body.appendChild(container);
15 | });
16 |
17 | afterEach(() => {
18 | document.body.removeChild(container);
19 | });
20 |
21 | function browserSupportsCssCustomProperties() {
22 | const style = document.createElement('div').style;
23 | style.setProperty('--prop', 'value');
24 | return style.getPropertyValue('--prop') === 'value';
25 | }
26 |
27 | function render(style) {
28 | elementVoid('div', null, null, 'style', style);
29 | }
30 |
31 | it('should render with the correct style properties for objects', () => {
32 | patch(container, () => render({color: 'white', backgroundColor: 'red'}));
33 | const el = container.childNodes[0];
34 |
35 | expect(el.style.color).to.equal('white');
36 | expect(el.style.backgroundColor).to.equal('red');
37 | });
38 |
39 | if (browserSupportsCssCustomProperties()) {
40 | it('should apply custom properties', () => {
41 | patch(container, () => render({'--some-var': 'blue'}));
42 | const el = container.childNodes[0];
43 |
44 | expect(el.style.getPropertyValue('--some-var')).to.equal('blue');
45 | });
46 | }
47 |
48 | it('should handle dashes in property names', () => {
49 | patch(container, () => render({'background-color': 'red'}));
50 | const el = container.childNodes[0];
51 |
52 | expect(el.style.backgroundColor).to.equal('red');
53 | });
54 |
55 | it('should update the correct style properties', () => {
56 | patch(container, () => render({color: 'white'}));
57 | patch(container, () => render({color: 'red'}));
58 | const el = container.childNodes[0];
59 |
60 | expect(el.style.color).to.equal('red');
61 | });
62 |
63 | it('should remove properties not present in the new object', () => {
64 | patch(container, () => render({color: 'white'}));
65 | patch(container, () => render({backgroundColor: 'red'}));
66 | const el = container.childNodes[0];
67 |
68 | expect(el.style.color).to.equal('');
69 | expect(el.style.backgroundColor).to.equal('red');
70 | });
71 |
72 | it('should render with the correct style properties for strings', () => {
73 | patch(container, () => render('color: white; background-color: red;'));
74 | const el = container.childNodes[0];
75 |
76 | expect(el.style.color).to.equal('white');
77 | expect(el.style.backgroundColor).to.equal('red');
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/test/functional/text_nodes_spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
2 | /** @license SPDX-License-Identifier: Apache-2.0 */
3 |
4 | // taze: mocha from //third_party/javascript/typings/mocha
5 | // taze: chai from //third_party/javascript/typings/chai
6 | import {elementOpenStart, patch, text} from '../../index';
7 | const {expect} = chai;
8 |
9 |
10 | describe('text nodes', () => {
11 | let container: HTMLDivElement;
12 |
13 | beforeEach(() => {
14 | container = document.createElement('div');
15 | document.body.appendChild(container);
16 | });
17 |
18 | afterEach(() => {
19 | document.body.removeChild(container);
20 | });
21 |
22 | describe('when created', () => {
23 | it('should render a text node with the specified value', () => {
24 | patch(container, () => {
25 | text('Hello world!');
26 | });
27 | const node = container.childNodes[0];
28 |
29 | expect(node.textContent).to.equal('Hello world!');
30 | expect(node).to.be.instanceof(Text);
31 | });
32 |
33 | it('should allow for multiple text nodes under one parent element', () => {
34 | patch(container, () => {
35 | text('Hello ');
36 | text('World');
37 | text('!');
38 | });
39 |
40 | expect(container.textContent).to.equal('Hello World!');
41 | });
42 |
43 | it('should throw when inside virtual attributes element', () => {
44 | expect(() => {
45 | patch(container, () => {
46 | elementOpenStart('div');
47 | text('Hello');
48 | });
49 | })
50 | .to.throw(
51 | 'text() can not be called between elementOpenStart()' +
52 | ' and elementOpenEnd().');
53 | });
54 | });
55 |
56 | describe('when updated after the DOM is updated', () => {
57 | // This avoids an Edge bug; see
58 | // https://github.com/google/incremental-dom/pull/398#issuecomment-497339108
59 | it('should do nothng', () => {
60 | patch(container, () => text('Hello'));
61 |
62 | container.firstChild!.nodeValue = 'Hello World!';
63 |
64 | const mo = new MutationObserver(() => {});
65 | mo.observe(container, {subtree: true, characterData: true});
66 |
67 | patch(container, () => text('Hello World!'));
68 | expect(mo.takeRecords()).to.be.empty;
69 | expect(container.textContent).to.equal('Hello World!');
70 | });
71 | });
72 |
73 | describe('with conditional text', () => {
74 | function render(data) {
75 | text(data);
76 | }
77 |
78 | it('should update the DOM when the text is updated', () => {
79 | patch(container, () => render('Hello'));
80 | patch(container, () => render('Hello World!'));
81 | const node = container.childNodes[0];
82 |
83 | expect(node.textContent).to.equal('Hello World!');
84 | });
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/test/functional/virtual_attributes_spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
2 | /** @license SPDX-License-Identifier: Apache-2.0 */
3 |
4 | // taze: chai from //third_party/javascript/typings/chai
5 | import {attr, elementClose, elementOpen, elementOpenEnd, elementOpenStart, patch} from '../../index';
6 | const {expect} = chai;
7 |
8 | describe('virtual attribute updates', () => {
9 | let container;
10 |
11 | beforeEach(() => {
12 | container = document.createElement('div');
13 | document.body.appendChild(container);
14 | });
15 |
16 | afterEach(() => {
17 | document.body.removeChild(container);
18 | });
19 |
20 | describe('for conditional attributes', () => {
21 | function render(obj) {
22 | elementOpenStart('div');
23 | if (obj.key) {
24 | attr('data-expanded', obj.key);
25 | }
26 | elementOpenEnd();
27 | elementClose('div');
28 | }
29 |
30 | it('should be present when specified', () => {
31 | patch(container, () => render({key: 'hello'}));
32 | const el = container.childNodes[0];
33 |
34 | expect(el.getAttribute('data-expanded')).to.equal('hello');
35 | });
36 |
37 | it('should be not present when not specified', () => {
38 | patch(container, () => render({key: false}));
39 | const el = container.childNodes[0];
40 |
41 | expect(el.getAttribute('data-expanded')).to.equal(null);
42 | });
43 |
44 | it('should update the DOM when they change', () => {
45 | patch(container, () => render({key: 'foo'}));
46 | patch(container, () => render({key: 'bar'}));
47 | const el = container.childNodes[0];
48 |
49 | expect(el.getAttribute('data-expanded')).to.equal('bar');
50 | });
51 |
52 | it('should correctly apply attributes during nested patches', () => {
53 | const otherContainer = document.createElement('div');
54 |
55 | patch(container, () => {
56 | elementOpenStart('div');
57 | attr('parrentAttrOne', 'pOne');
58 |
59 | patch(otherContainer, () => {
60 | elementOpenStart('div');
61 | attr('childAttrOne', 'cOne');
62 | elementOpenEnd();
63 | elementClose('div');
64 | });
65 |
66 | attr('parrentAttrTwo', 'pTwo');
67 | elementOpenEnd();
68 |
69 | elementClose('div');
70 | });
71 |
72 | const parentAttributes = container.children[0].attributes;
73 | expect(parentAttributes).to.have.length(2);
74 | expect(parentAttributes['parrentAttrOne'].value).to.equal('pOne');
75 | expect(parentAttributes['parrentAttrTwo'].value).to.equal('pTwo');
76 | const childAttributes = otherContainer.children[0].attributes;
77 | expect(childAttributes).to.have.length(1);
78 | expect(childAttributes['childAttrOne'].value).to.equal('cOne');
79 | });
80 | });
81 |
82 | it('should throw when a virtual attributes element is unclosed', () => {
83 | expect(() => {
84 | patch(container, () => {
85 | elementOpenStart('div');
86 | });
87 | })
88 | .to.throw(
89 | 'elementOpenEnd() must be called after calling' +
90 | ' elementOpenStart().');
91 | });
92 |
93 | it(`should throw when virtual attributes element is
94 | closed without being opened`,
95 | () => {
96 | expect(() => {
97 | patch(container, () => {
98 | elementOpenEnd();
99 | });
100 | })
101 | .to.throw(
102 | 'elementOpenEnd() can only be called' +
103 | ' after calling elementOpenStart().');
104 | });
105 |
106 | it('should throw when opening an element inside a virtual attributes element',
107 | () => {
108 | expect(() => {
109 | patch(container, () => {
110 | elementOpenStart('div');
111 | elementOpen('div');
112 | });
113 | })
114 | .to.throw(
115 | 'elementOpen() can not be called between' +
116 | ' elementOpenStart() and elementOpenEnd().');
117 | });
118 |
119 | it('should throw when opening a virtual attributes element' +
120 | ' inside a virtual attributes element',
121 | () => {
122 | expect(() => {
123 | patch(container, () => {
124 | elementOpenStart('div');
125 | elementOpenStart('div');
126 | });
127 | })
128 | .to.throw(
129 | 'elementOpenStart() can not be called between' +
130 | ' elementOpenStart() and elementOpenEnd().');
131 | });
132 |
133 | it('should throw when closing an element inside a virtual attributes element',
134 | () => {
135 | expect(() => {
136 | patch(container, () => {
137 | elementOpenStart('div');
138 | elementClose('div');
139 | });
140 | })
141 | .to.throw(
142 | 'elementClose() can not be called between' +
143 | ' elementOpenStart() and elementOpenEnd().');
144 | });
145 | });
146 |
--------------------------------------------------------------------------------
/test/integration/keyed_items_spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
2 | /** @license SPDX-License-Identifier: Apache-2.0 */
3 |
4 | import {elementOpen, elementClose, patch} from '../../index';
5 | import * as Sinon from 'sinon';
6 |
7 | const {expect} = chai;
8 |
9 | /*
10 | * These tests just capture the current state of mutations that occur when
11 | * changing the items. These could change in the future.
12 | */
13 | describe('keyed items', () => {
14 | let container: HTMLElement;
15 | const sandbox = Sinon.sandbox.create();
16 | const mutationObserverConfig = {
17 | childList: true,
18 | subtree: true,
19 | };
20 |
21 | beforeEach(() => {
22 | container = document.createElement('div');
23 | document.body.appendChild(container);
24 | });
25 |
26 | afterEach(() => {
27 | sandbox.restore();
28 | document.body.removeChild(container);
29 | });
30 |
31 | /**
32 | * @param container
33 | */
34 | function createMutationObserver(container: Element): MutationObserver {
35 | const mo = new MutationObserver(() => {});
36 | mo.observe(container, mutationObserverConfig);
37 |
38 | return mo;
39 | }
40 |
41 | /**
42 | * @param keys
43 | */
44 | function render(keys: number[]) {
45 | keys.forEach((key) => {
46 | elementOpen('div', key);
47 | elementClose('div');
48 | });
49 | }
50 |
51 | it('should cause no mutations when the items stay the same', () => {
52 | patch(container, () => render([1, 2, 3]));
53 |
54 | const mo = createMutationObserver(container);
55 | patch(container, () => render([1, 2, 3]));
56 |
57 | const records = mo.takeRecords();
58 | expect(records).to.be.empty;
59 | });
60 |
61 | it('causes only one mutation when adding a new item', () => {
62 | patch(container, () => render([1, 2, 3]));
63 |
64 | const mo = createMutationObserver(container);
65 | patch(container, () => render([0, 1, 2, 3]));
66 |
67 | const records = mo.takeRecords();
68 | expect(records).to.have.length(1);
69 | });
70 |
71 | it('cause a removal and addition when moving forwards', () => {
72 | patch(container, () => render([1, 2, 3]));
73 |
74 | const mo = createMutationObserver(container);
75 | patch(container, () => render([3, 1, 2]));
76 |
77 | const records = mo.takeRecords();
78 | expect(records).to.have.length(2);
79 | expect(records[0].addedNodes).to.have.length(0);
80 | expect(records[0].removedNodes).to.have.length(1);
81 | expect(records[1].addedNodes).to.have.length(1);
82 | expect(records[1].removedNodes).to.have.length(0);
83 | });
84 |
85 | it('causes mutations for each item when removing from the start', () => {
86 | patch(container, () => render([1, 2, 3, 4]));
87 |
88 | const mo = createMutationObserver(container);
89 | patch(container, () => render([2, 3, 4]));
90 |
91 | const records = mo.takeRecords();
92 | // 7 Mutations: two for each of the nodes moving forward and one for the
93 | // removal.
94 | expect(records).to.have.length(7);
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/test/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | // Karma tests fail with a Chrome sandbox error on macOS, so disable the
3 | // sandbox.
4 | if (process.platform === 'darwin') {
5 | config.set({
6 | browsers: ['ChromeHeadless_macos_fixed'],
7 | customLaunchers: {
8 | ChromeHeadless_macos_fixed: {
9 | base: 'ChromeHeadless',
10 | flags: [
11 | '--no-sandbox',
12 | ],
13 | },
14 | },
15 | });
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/test/unit/changes_spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
2 | /** @license SPDX-License-Identifier: Apache-2.0 */
3 |
4 | // taze: chai from //third_party/javascript/typings/chai
5 |
6 | import * as Sinon from 'sinon';
7 |
8 | import {flush, queueChange} from '../../src/changes';
9 |
10 | const {expect} = chai;
11 |
12 | describe('changes', () => {
13 | it('should call the update functions for all changes', () => {
14 | const spyOne = Sinon.spy();
15 | const spyTwo = Sinon.spy();
16 |
17 | queueChange(spyOne, 'a', 'b', 'c', 'd');
18 | queueChange(spyTwo, 'd', 'e', 'f', 'g');
19 | flush();
20 |
21 | expect(spyOne).to.have.been.calledOnce.to.have.been.calledWith(
22 | 'a', 'b', 'c', 'd');
23 | expect(spyTwo).to.have.been.calledOnce.to.have.been.calledWith(
24 | 'd', 'e', 'f', 'g');
25 | });
26 |
27 | it('should clear the changes after flush', () => {
28 | const spy = Sinon.spy();
29 | queueChange(spy, 'a', 'b', 'c', 'd');
30 | flush();
31 | flush();
32 |
33 | expect(spy).to.have.been.calledOnce.to.have.been.calledWith('a', 'b', 'c', 'd');
34 | });
35 |
36 | it('should allow re-entrant usage', () => {
37 | const innerSpy = Sinon.spy();
38 | const outerSpyOne = Sinon.spy(() => {
39 | queueChange(innerSpy, 'd', 'e', 'f', 'g');
40 | queueChange(innerSpy, 'g', 'h', 'i', 'j');
41 | flush();
42 | });
43 | const outerSpyTwo = Sinon.spy();
44 |
45 | queueChange(outerSpyOne, 'a', 'b', 'c', 'd');
46 |
47 | queueChange(outerSpyTwo, 'j', 'k', 'l', 'm');
48 | flush();
49 |
50 | expect(innerSpy)
51 | .to.have.been.calledTwice.to.have.been.calledWith('d', 'e', 'f', 'g')
52 | .to.have.been.calledWith('g', 'h', 'i', 'j');
53 | expect(outerSpyOne)
54 | .to.have.been.calledOnce.to.have.been.calledWith('a', 'b', 'c', 'd');
55 | expect(outerSpyTwo)
56 | .to.have.been.calledOnce.to.have.been.calledWith('j', 'k', 'l', 'm');
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/test/unit/diff_spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
2 | /** @license SPDX-License-Identifier: Apache-2.0 */
3 |
4 | // taze: chai from //third_party/javascript/typings/chai
5 | import * as Sinon from 'sinon';
6 | import {calculateDiff} from '../../src/diff';
7 | import {attributes} from '../../src/attributes';
8 | const {expect} = chai;
9 |
10 | describe('calculateDiff', () => {
11 | const updateCtx = {};
12 | let updateFn: Sinon.SinonSpy;
13 |
14 | beforeEach(() => {
15 | updateFn = Sinon.spy();
16 | });
17 |
18 | it('should call the update function for added items', () => {
19 | const prev: string[] = [];
20 | const next = ['name1', 'value1', 'name2', 'value2'];
21 |
22 | calculateDiff(prev, next, updateCtx, updateFn, attributes);
23 |
24 | expect(updateFn)
25 | .to.have.been.calledTwice.to.have.been
26 | .calledWith(updateCtx, 'name1', 'value1')
27 | .to.have.been.calledWith(updateCtx, 'name2', 'value2');
28 | });
29 |
30 | it('should call the update function for removed items', () => {
31 | const prev = ['name1', 'value1', 'name2', 'value2'];
32 | const next: string[] = [];
33 |
34 | calculateDiff(prev, next, updateCtx, updateFn, attributes);
35 |
36 | expect(updateFn)
37 | .to.have.been.calledTwice.to.have.been
38 | .calledWith(updateCtx, 'name1', undefined)
39 | .to.have.been.calledWith(updateCtx, 'name2', undefined);
40 | });
41 |
42 | it('should not call the update function if there are no changes', () => {
43 | const prev = ['name', 'value'];
44 | const next = ['name', 'value'];
45 |
46 | calculateDiff(prev, next, updateCtx, updateFn, attributes);
47 |
48 | expect(updateFn).to.have.been.not.called;
49 | });
50 |
51 | it('should handle items appearing earlier', () => {
52 | const prev = ['name1', 'value1'];
53 | const next = ['name2', 'value2', 'name1', 'value1'];
54 |
55 | calculateDiff(prev, next, updateCtx, updateFn, attributes);
56 |
57 | expect(updateFn).to.have.been.calledOnce.to.have.been.calledWith(
58 | updateCtx, 'name2', 'value2');
59 | });
60 |
61 | it('should handle changed item ordering', () => {
62 | const prev = ['name1', 'value1', 'name2', 'value2'];
63 | const next = ['name2', 'value2', 'name1', 'value1'];
64 |
65 | calculateDiff(prev, next, updateCtx, updateFn, attributes);
66 |
67 | expect(updateFn).to.have.been.not.called;
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/test/util/dom.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
2 | /** @license SPDX-License-Identifier: Apache-2.0 */
3 |
4 | const BROWSER_SUPPORTS_SHADOW_DOM = 'ShadowRoot' in window;
5 |
6 |
7 | const attachShadow = (el: HTMLElement) => {
8 | return el.attachShadow ? el.attachShadow({mode: 'closed'}) :
9 | // tslint:disable:no-any
10 | (el as any).createShadowRoot();
11 | };
12 |
13 | function assertElement(el: Node|null): Element {
14 | if (el instanceof Element) {
15 | return el;
16 | }
17 | throw new Error('Expected element to be Element');
18 | }
19 |
20 | function assertHTMLElement(el: Node|null): HTMLElement {
21 | if (el instanceof HTMLElement) {
22 | return el;
23 | }
24 | throw new Error('Expected element to be HTMLElement');
25 | }
26 |
27 |
28 | export {
29 | BROWSER_SUPPORTS_SHADOW_DOM,
30 | assertElement,
31 | assertHTMLElement,
32 | attachShadow
33 | };
34 |
--------------------------------------------------------------------------------
/test/util/globals.js:
--------------------------------------------------------------------------------
1 | // Copyright 2017 The Incremental DOM Authors. All Rights Reserved.
2 | /** @license SPDX-License-Identifier: Apache-2.0 */
3 |
4 | // This is in a separate file as a global to prevent Babel from transpiling
5 | // classes. Transpiled classes do not work with customElements.define.
6 | if (window.customElements) {
7 | window.MyElementDefine = class extends HTMLElement {
8 | constructor() {
9 | super();
10 | }
11 | };
12 |
13 | window.customElements.define('my-element-define', window.MyElementDefine);
14 | }
15 |
16 | if (document.registerElement) {
17 | window.MyElementRegister = document.registerElement('my-element-register', {
18 | prototype: Object.create(HTMLElement.prototype)
19 | });
20 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "rootDir": ".",
4 | "outDir": "genfiles",
5 | "allowSyntheticDefaultImports": false,
6 | "allowUnreachableCode": false,
7 | "allowUnusedLabels": false,
8 | "declaration": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "noEmitOnError": true,
12 | "noImplicitAny": false,
13 | "noImplicitReturns": true,
14 | "pretty": true,
15 | "strict": true,
16 | "module": "commonjs",
17 | "target": "es5",
18 | "lib": ["es2015", "dom"],
19 | "sourceMap": true
20 | },
21 | "include": [
22 | "index.ts",
23 | "src/*.ts",
24 | "test/*.ts",
25 | "test/**/*.ts"
26 | ],
27 | "exclude": [
28 | "node_modules"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------