1// Copyright 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import * as LitHtml from '../../../../front_end/third_party/lit-html/lit-html.js';
6
7import {renderElementIntoDOM} from './DOMHelpers.js';
8import {TEXT_NODE, withMutations, withNoMutations} from './MutationHelpers.js';
9
10const {assert} = chai;
11
12/**
13 * Needed because assert.throws from chai does not work async.
14 */
15async function assertThrowsAsync(fn: () => Promise<void>, errorMessage: string) {
16  let caught = false;
17  try {
18    await fn();
19  } catch (e) {
20    caught = true;
21    assert.strictEqual(e.message, errorMessage);
22  }
23
24  if (!caught) {
25    assert.fail('Expected error but got none.');
26  }
27}
28
29async function assertNotThrowsAsync(fn: () => Promise<void>) {
30  let errorMessage = '';
31  try {
32    await fn();
33  } catch (e) {
34    errorMessage = e.message;
35  }
36
37  if (errorMessage) {
38    assert.fail(`Expected no error but got:\n${errorMessage}`);
39  }
40}
41
42describe('MutationHelpers', () => {
43  describe('withMutations', () => {
44    it('fails if there are no mutations', async () => {
45      const div = document.createElement('div');
46      await assertThrowsAsync(async () => {
47        await withMutations(
48            [{
49              target: 'div',
50            }],
51            div, () => {});
52      }, 'Expected at least one mutation for ADD/REMOVE div, but got 0');
53    });
54
55    it('allows up to 10 mutations unless specified', async () => {
56      const div = document.createElement('div');
57      renderElementIntoDOM(div);
58      await assertNotThrowsAsync(async () => {
59        await withMutations(
60            [{
61              target: 'div',
62            }],
63            div, () => {
64              for (let i = 0; i < 10; i++) {
65                div.appendChild(document.createElement('div'));
66              }
67            });
68      });
69    });
70
71    it('errors if there are >10 mutations', async () => {
72      const div = document.createElement('div');
73      renderElementIntoDOM(div);
74      await assertThrowsAsync(async () => {
75        await withMutations(
76            [{
77              target: 'div',
78            }],
79            div, () => {
80              for (let i = 0; i < 11; i++) {
81                div.appendChild(document.createElement('div'));
82              }
83            });
84      }, 'Expected no more than 10 mutations for ADD/REMOVE div, but got 11');
85    });
86
87    it('lets the user provide the max', async () => {
88      const div = document.createElement('div');
89      renderElementIntoDOM(div);
90      await assertThrowsAsync(async () => {
91        await withMutations(
92            [{
93              target: 'div',
94              max: 5,
95            }],
96            div, () => {
97              for (let i = 0; i < 6; i++) {
98                div.appendChild(document.createElement('div'));
99              }
100            });
101      }, 'Expected no more than 5 mutations for ADD/REMOVE div, but got 6');
102    });
103
104    it('supports a max of 0', async () => {
105      const div = document.createElement('div');
106      renderElementIntoDOM(div);
107      await assertThrowsAsync(async () => {
108        await withMutations(
109            [{
110              target: 'div',
111              max: 0,
112            }],
113            div, () => {
114              div.appendChild(document.createElement('div'));
115            });
116      }, 'Expected no more than 0 mutations for ADD/REMOVE div, but got 1');
117    });
118
119    it('supports checking multiple expected mutations', async () => {
120      const div = document.createElement('div');
121      renderElementIntoDOM(div);
122      await assertThrowsAsync(async () => {
123        await withMutations(
124            [
125              {
126                target: 'div',
127                max: 1,
128              },
129              {target: 'span', max: 0},
130            ],
131            div, () => {
132              div.appendChild(document.createElement('div'));
133              div.appendChild(document.createElement('span'));
134            });
135      }, 'Expected no more than 0 mutations for ADD/REMOVE span, but got 1');
136    });
137
138    it('errors if other unexpected mutations occur', async () => {
139      const div = document.createElement('div');
140      renderElementIntoDOM(div);
141      await assertThrowsAsync(async () => {
142        await withMutations(
143            [{
144              target: 'div',
145              max: 1,
146            }],
147            div, () => {
148              // this is OK as we are expecting one div mutation
149              div.appendChild(document.createElement('div'));
150              // not OK - we have not declared any span mutations
151              div.appendChild(document.createElement('span'));
152            });
153      }, 'Additional unexpected mutations were detected:\nspan: 1 addition');
154    });
155
156    it('lets you declare any expected text updates', async () => {
157      const div = document.createElement('div');
158      const renderList = (list: string[]) => {
159        const html = LitHtml.html`${list.map(l => LitHtml.html`<span>${l}</span>`)}`;
160        LitHtml.render(html, div);
161      };
162
163      renderElementIntoDOM(div);
164      renderList(['a', 'b']);
165
166      await assertNotThrowsAsync(async () => {
167        await withMutations(
168            [
169              {
170                target: 'div',
171              },
172              {target: TEXT_NODE},
173            ],
174            div, div => {
175              renderList(['b', 'a']);
176              div.appendChild(document.createElement('div'));
177            });
178      });
179    });
180
181    it('fails if there are undeclared text updates', async () => {
182      const div = document.createElement('div');
183      const renderList = (list: string[]) => {
184        const html = LitHtml.html`${list.map(l => LitHtml.html`<span>${l}</span>`)}`;
185        LitHtml.render(html, div);
186      };
187
188      renderElementIntoDOM(div);
189      renderList(['a', 'b']);
190
191      await assertThrowsAsync(async () => {
192        await withMutations(
193            [{
194              target: 'div',
195            }],
196            div, div => {
197              renderList(['b', 'a']);
198              div.appendChild(document.createElement('div'));
199            });
200      }, 'Additional unexpected mutations were detected:\nTEXT_NODE: 2 updates');
201    });
202  });
203
204  describe('withNoMutations', () => {
205    it('fails if there are DOM additions', async () => {
206      const div = document.createElement('div');
207      renderElementIntoDOM(div);
208      await assertThrowsAsync(async () => {
209        await withNoMutations(div, element => {
210          const child = document.createElement('span');
211          element.appendChild(child);
212        });
213      }, 'Expected no mutations, but got 1: \nspan: 1 addition');
214    });
215
216    it('fails if there are DOM removals', async () => {
217      const div = document.createElement('div');
218      const child = document.createElement('span');
219      div.appendChild(child);
220      renderElementIntoDOM(div);
221
222      await assertThrowsAsync(async () => {
223        await withNoMutations(div, element => {
224          element.removeChild(child);
225        });
226      }, 'Expected no mutations, but got 1: \nspan: 1 removal');
227    });
228
229    it('correctly displays multiple unexpected mutations', async () => {
230      const div = document.createElement('div');
231      renderElementIntoDOM(div);
232      await assertThrowsAsync(async () => {
233        await withNoMutations(div, element => {
234          const child = document.createElement('span');
235          element.appendChild(child);
236          element.removeChild(child);
237          element.appendChild(document.createElement('p'));
238          element.appendChild(document.createElement('p'));
239          element.appendChild(document.createElement('p'));
240        });
241      }, 'Expected no mutations, but got 5: \nspan: 1 addition, 1 removal\np: 3 additions');
242    });
243
244    it('fails if there are text re-orderings', async () => {
245      const div = document.createElement('div');
246      const renderList = (list: string[]) => {
247        const html = LitHtml.html`${list.map(l => LitHtml.html`<span>${l}</span>`)}`;
248        LitHtml.render(html, div);
249      };
250
251      renderElementIntoDOM(div);
252      renderList(['a', 'b']);
253
254      await assertThrowsAsync(async () => {
255        await withNoMutations(div, () => {
256          renderList(['b', 'a']);
257        });
258      }, 'Expected no mutations, but got 2: \nTEXT_NODE: 2 updates');
259    });
260
261    it('fails if there are text re-orderings and DOM additions', async () => {
262      const div = document.createElement('div');
263      const renderList = (list: string[]) => {
264        const html = LitHtml.html`${list.map(l => LitHtml.html`<span>${l}</span>`)}`;
265        LitHtml.render(html, div);
266      };
267
268      renderElementIntoDOM(div);
269      renderList(['a', 'b']);
270
271      await assertThrowsAsync(async () => {
272        await withNoMutations(div, div => {
273          renderList(['b', 'a']);
274          div.appendChild(document.createElement('ul'));
275        });
276      }, 'Expected no mutations, but got 3: \nTEXT_NODE: 2 updates\nul: 1 addition');
277    });
278  });
279});
280