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