| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>innerHTML Cache Mutation and Invalidation Test</title> |
| <script> |
| if (window.testRunner) { |
| testRunner.dumpAsText(); |
| } |
| |
| function log(msg) { |
| console.log(msg) |
| document.getElementById('console').appendChild(document.createTextNode(msg + '\n')); |
| } |
| |
| function testPrefixReuse() { |
| log('=== Basic prefix extension ==='); |
| const container = document.createElement('div'); |
| document.body.appendChild(container); |
| container.innerHTML = '<div>base</div>'; |
| container.innerHTML = '<div>base</div><span>extended</span>'; |
| log('Set base then extend with prefix match'); |
| log('children.length: ' + container.children.length + ' (expected: 2)'); |
| log('Result: ' + (container.children.length === 2 ? 'PASS' : 'FAIL') + '\n'); |
| container.innerHTML = ''; |
| |
| log('=== Multiple extensions ==='); |
| const listContainer = document.createElement('div'); |
| document.body.appendChild(listContainer); |
| listContainer.innerHTML = '<ul><li>1</li></ul>'; |
| listContainer.innerHTML = '<ul><li>1</li></ul><ul><li>2</li></ul>'; |
| listContainer.innerHTML = '<ul><li>1</li></ul><ul><li>2</li></ul><ul><li>3</li></ul>'; |
| log('Extend by adding complete lists'); |
| log('ul count: ' + listContainer.querySelectorAll('ul').length + ' (expected: 3)'); |
| log('Result: ' + (listContainer.querySelectorAll('ul').length === 3 ? 'PASS' : 'FAIL') + '\n'); |
| listContainer.innerHTML = ''; |
| |
| log('=== Exact match (cache hit) ==='); |
| const exactContainer = document.createElement('div'); |
| document.body.appendChild(exactContainer); |
| exactContainer.innerHTML = '<div>content</div><span>more</span>'; |
| exactContainer.innerHTML = '<div>content</div><span>more</span>'; |
| log('Set same innerHTML twice'); |
| log('children.length: ' + exactContainer.children.length + ' (expected: 2)'); |
| log('Result: ' + (exactContainer.children.length === 2 ? 'PASS' : 'FAIL') + '\n'); |
| exactContainer.innerHTML = ''; |
| } |
| |
| function testDOMMutations() { |
| log('---------- DOM Mutation Tests ----------\n'); |
| |
| log('=== appendChild invalidates cache ==='); |
| const appendContainer = document.createElement('div'); |
| document.body.appendChild(appendContainer); |
| appendContainer.innerHTML = '<div id="append-test">original</div>'; |
| appendContainer.innerHTML = '<div id="append-test">original</div><span>extension</span>'; |
| // Mutate the cached DOM by appending a <b> element |
| const newEl = document.createElement('b'); |
| newEl.textContent = 'added'; |
| appendContainer.querySelector('#append-test').appendChild(newEl); |
| // Because the cache was invalidated by the appendChild mutation, |
| // this creates fresh nodes from scratch |
| appendContainer.innerHTML = '<div id="append-test">original</div><span>extension</span><p>more</p>'; |
| log('appendChild to cached element, then extend'); |
| // Check if the mutation carried over |
| // The test PASSES if there's no <b> element in #append-test |
| // This proves the cache was invalidated and fresh nodes were created |
| log('Result: ' + (!appendContainer.querySelector('#append-test b') ? 'PASS' : 'FAIL') + '\n'); |
| appendContainer.innerHTML = ''; |
| |
| log('=== insertAdjacentHTML invalidates cache ==='); |
| const adjacentContainer = document.createElement('div'); |
| document.body.appendChild(adjacentContainer); |
| adjacentContainer.innerHTML = '<div id="adjacent-test">content</div>'; |
| adjacentContainer.innerHTML = '<div id="adjacent-test">content</div><p>extension</p>'; |
| // Mutate the cached DOM by inserting HTML into #adjacent-test |
| adjacentContainer.querySelector('#adjacent-test').insertAdjacentHTML('beforeend', '<span class="strong">inserted</span>'); |
| // Because the cache was invalidated by insertAdjacentHTML, |
| // this creates fresh nodes from scratch |
| adjacentContainer.innerHTML = '<div id="adjacent-test">content</div><p>extension</p><div>more</div>'; |
| log('insertAdjacentHTML then extend'); |
| // Check if the inserted content carried over |
| // The test PASSES if there's no .strong element in #adjacent-test |
| log('Result: ' + (!adjacentContainer.querySelector('#adjacent-test .strong') ? 'PASS' : 'FAIL') + '\n'); |
| adjacentContainer.innerHTML = ''; |
| |
| log('=== outerHTML invalidates cache ==='); |
| const outerContainer = document.createElement('div'); |
| document.body.appendChild(outerContainer); |
| outerContainer.innerHTML = '<div id="outer-test">content</div>'; |
| outerContainer.innerHTML = '<div id="outer-test">content</div><span>extension</span>'; |
| // Replace the cached element entirely with outerHTML (changes it from div to p) |
| outerContainer.querySelector('#outer-test').outerHTML = '<p id="outer-test">replaced</p>'; |
| // Because the cache was invalidated by the outerHTML replacement, |
| // this creates fresh nodes from scratch |
| outerContainer.innerHTML = '<div id="outer-test">content</div><span>extension</span><div class="footer">more</div>'; |
| log('outerHTML replacement then extend'); |
| // Check if the element is back to being a DIV (not a P) |
| // The test PASSES if it's a DIV, proving fresh nodes were created |
| log('outer-test.tagName: ' + outerContainer.querySelector('#outer-test').tagName + ' (expected: DIV)'); |
| log('Result: ' + (outerContainer.querySelector('#outer-test').tagName === 'DIV' ? 'PASS' : 'FAIL') + '\n'); |
| outerContainer.innerHTML = ''; |
| |
| log('=== removeChild invalidates cache ==='); |
| const removeContainer = document.createElement('div'); |
| document.body.appendChild(removeContainer); |
| removeContainer.innerHTML = '<div id="remove-test"><span>child</span></div>'; |
| removeContainer.innerHTML = '<div id="remove-test"><span>child</span></div><p>extension</p>'; |
| // Remove the span element from the cached DOM |
| const childToRemove = removeContainer.querySelector('#remove-test span'); |
| removeContainer.querySelector('#remove-test').removeChild(childToRemove); |
| // Because the cache was invalidated by removeChild, |
| // this creates fresh nodes from scratch |
| removeContainer.innerHTML = '<div id="remove-test"><span>child</span></div><p>extension</p><div class="footer">more</div>'; |
| log('removeChild then extend'); |
| // Check if the span is back (it should be, proving fresh nodes were created) |
| log('remove-test has span: ' + (removeContainer.querySelector('#remove-test span') !== null) + ' (expected: true)'); |
| log('Result: ' + (removeContainer.querySelector('#remove-test span') !== null ? 'PASS' : 'FAIL') + '\n'); |
| removeContainer.innerHTML = ''; |
| |
| log('=== textContent change invalidates cache ==='); |
| const textContainer = document.createElement('div'); |
| document.body.appendChild(textContainer); |
| textContainer.innerHTML = '<div id="text-test">original</div>'; |
| textContainer.innerHTML = '<div id="text-test">original</div><span>extension</span>'; |
| textContainer.querySelector('#text-test').textContent = 'changed'; |
| textContainer.innerHTML = '<div id="text-test">original</div><span>extension</span><p>more</p>'; |
| log('textContent change then extend'); |
| log('text-test.textContent: "' + textContainer.querySelector('#text-test').textContent + '" (expected: "original")'); |
| log('Result: ' + (textContainer.querySelector('#text-test').textContent === 'original' ? 'PASS' : 'FAIL') + '\n'); |
| textContainer.innerHTML = ''; |
| |
| log('=== Descendant mutation invalidates cache ==='); |
| const nestedContainer = document.createElement('div'); |
| document.body.appendChild(nestedContainer); |
| nestedContainer.innerHTML = '<div id="outer-nested"><div id="middle"><span id="inner-nested">text</span></div></div>'; |
| nestedContainer.innerHTML = '<div id="outer-nested"><div id="middle"><span id="inner-nested">text</span></div></div><p>extension</p>'; |
| nestedContainer.querySelector('#inner-nested').textContent = 'changed'; |
| nestedContainer.innerHTML = '<div id="outer-nested"><div id="middle"><span id="inner-nested">text</span></div></div><p>extension</p><div class="footer">more</div>'; |
| log('Mutated grandchild element then extend'); |
| log('inner-nested.textContent: "' + nestedContainer.querySelector('#inner-nested').textContent + '" (expected: "text")'); |
| log('Result: ' + (nestedContainer.querySelector('#inner-nested').textContent === 'text' ? 'PASS' : 'FAIL') + '\n'); |
| nestedContainer.innerHTML = ''; |
| } |
| |
| function testAttributeMutations() { |
| log('---------- Attribute Mutation Tests ----------\n'); |
| |
| log('=== Inline style change invalidates cache ==='); |
| const colorContainer = document.createElement('div'); |
| document.body.appendChild(colorContainer); |
| // Note: Avoiding inline style attributes in initial content for fast parser compatibility |
| colorContainer.innerHTML = '<div id="style-color-test">text</div>'; |
| // Add style programmatically to the element |
| colorContainer.querySelector('#style-color-test').style.color = 'red'; |
| colorContainer.innerHTML = '<div id="style-color-test">text</div><span>extension</span>'; |
| // Mutate the cached element's style |
| colorContainer.querySelector('#style-color-test').style.color = 'blue'; |
| // Because the cache was invalidated by the style mutation, |
| // this creates fresh nodes from scratch |
| colorContainer.innerHTML = '<div id="style-color-test">text</div><span>extension</span><p>more</p>'; |
| log('Changed style.color to blue, then extended'); |
| // Check if the style carried over |
| // The test PASSES if style.color is empty (no inline style) |
| const finalColor = colorContainer.querySelector('#style-color-test').style.color; |
| log('Final color: ' + finalColor + ' (expected: empty/default)'); |
| log('Result: ' + (finalColor === '' ? 'PASS' : 'FAIL') + '\n'); |
| colorContainer.innerHTML = ''; |
| |
| log('=== style.display change invalidates cache ==='); |
| const displayContainer = document.createElement('div'); |
| document.body.appendChild(displayContainer); |
| displayContainer.innerHTML = '<div id="style-display-test">visible</div>'; |
| displayContainer.querySelector('#style-color-testb').style.display = 'block'; |
| displayContainer.innerHTML = '<div id="style-display-test">visible</div><span>extension</span>'; |
| displayContainer.querySelector('#style-color-testb').style.display = 'none'; |
| displayContainer.innerHTML = '<div id="style-display-test">visible</div><span>extension</span><p>more</p>'; |
| log('Changed style.display to none, then extended'); |
| const finalDisplay = displayContainer.querySelector('#style-color-testb').style.display; |
| log('Final display: ' + finalDisplay + ' (expected: empty/default)'); |
| log('Result: ' + (finalDisplay === '' ? 'PASS' : 'FAIL') + '\n'); |
| displayContainer.innerHTML = ''; |
| |
| log('=== className change invalidates cache ==='); |
| const classContainer = document.createElement('div'); |
| document.body.appendChild(classContainer); |
| classContainer.innerHTML = '<div id="class-test" class="original">text</div>'; |
| classContainer.innerHTML = '<div id="class-test" class="original">text</div><span>extension</span>'; |
| classContainer.querySelector('#class-test').className = 'changed'; |
| classContainer.innerHTML = '<div id="class-test" class="original">text</div><span>extension</span><p>more</p>'; |
| log('Changed className then extended'); |
| log('m7.className: "' + classContainer.querySelector('#class-test').className + '" (expected: "original")'); |
| log('Result: ' + (classContainer.querySelector('#class-test').className === 'original' ? 'PASS' : 'FAIL') + '\n'); |
| classContainer.innerHTML = ''; |
| |
| log('=== setAttribute invalidates cache ==='); |
| const attrContainer = document.createElement('div'); |
| document.body.appendChild(attrContainer); |
| // Using id attribute which is supported by fast parser |
| attrContainer.innerHTML = '<div id="attr-test">text</div>'; |
| attrContainer.innerHTML = '<div id="attr-test">text</div><span>extension</span>'; |
| // Add a data attribute to the cached element |
| attrContainer.querySelector('#attr-test').setAttribute('data-value', '999'); |
| // Because the cache was invalidated by setAttribute, |
| // this creates fresh nodes from scratch |
| attrContainer.innerHTML = '<div id="attr-test">text</div><span>extension</span><p>more</p>'; |
| log('Added attribute then extended'); |
| // Check if the data-value attribute carried over |
| // The test PASSES if there's no data-value attribute |
| const hasDataValue = attrContainer.querySelector('#attr-test').hasAttribute('data-value'); |
| log('Has data-value attribute: ' + hasDataValue + ' (expected: false)'); |
| log('Result: ' + (!hasDataValue ? 'PASS' : 'FAIL') + '\n'); |
| attrContainer.innerHTML = ''; |
| |
| log('=== Descendant attribute change invalidates cache ==='); |
| const nestedAttrContainer = document.createElement('div'); |
| document.body.appendChild(nestedAttrContainer); |
| nestedAttrContainer.innerHTML = '<div id="nested-attr-outer"><ul><li id="nested-attr-item" class="original">item</li></ul></div>'; |
| nestedAttrContainer.innerHTML = '<div id="nested-attr-outer"><ul><li id="nested-attr-item" class="original">item</li></ul></div><span>extension</span>'; |
| nestedAttrContainer.querySelector('#nested-attr-item').className = 'changed'; |
| nestedAttrContainer.innerHTML = '<div id="nested-attr-outer"><ul><li id="nested-attr-item" class="original">item</li></ul></div><span>extension</span><div class="footer">more</div>'; |
| log('Changed nested element className then extended'); |
| log('nested.className: "' + nestedAttrContainer.querySelector('#nested-attr-item').className + '" (expected: "original")'); |
| log('Result: ' + (nestedAttrContainer.querySelector('#nested-attr-item').className === 'original' ? 'PASS' : 'FAIL') + '\n'); |
| nestedAttrContainer.innerHTML = ''; |
| } |
| |
| function testStringLength() { |
| log('---------- String Length Tests ----------\n'); |
| |
| log('=== Shorter string invalidates cache ==='); |
| const shorterContainer = document.createElement('div'); |
| document.body.appendChild(shorterContainer); |
| const longHTML = '<div>base</div><span>extra</span>'; |
| const shortHTML = '<div>base</div>'; |
| // Set long HTML string (caches it) |
| shorterContainer.innerHTML = longHTML; |
| // Set shorter HTML string (should invalidate cache since it's not a prefix extension) |
| shorterContainer.innerHTML = shortHTML; |
| // Set long HTML string again (should create fresh nodes, not reuse old cache) |
| shorterContainer.innerHTML = longHTML; |
| log('Set long -> short -> long'); |
| // If cache was properly invalidated, we should have exactly 2 fresh children |
| log('children.length: ' + shorterContainer.children.length + ' (expected: 2)'); |
| log('Result: ' + (shorterContainer.children.length === 2 ? 'PASS' : 'FAIL') + '\n'); |
| shorterContainer.innerHTML = ''; |
| |
| log('=== Empty string invalidates cache ==='); |
| const emptyContainer = document.createElement('div'); |
| document.body.appendChild(emptyContainer); |
| emptyContainer.innerHTML = '<div>content</div>'; |
| emptyContainer.innerHTML = ''; |
| emptyContainer.innerHTML = '<div>content</div>'; |
| log('Set content -> empty -> content'); |
| log('children.length: ' + emptyContainer.children.length + ' (expected: 1)'); |
| log('Result: ' + (emptyContainer.children.length === 1 ? 'PASS' : 'FAIL') + '\n'); |
| // No need to reset here since the test already uses empty string |
| } |
| |
| function testShadowDOM() { |
| log('---------- Shadow DOM Tests ----------\n'); |
| |
| log('=== Shadow DOM innerHTML caching ==='); |
| const shadowHost1 = document.createElement('div'); |
| document.body.appendChild(shadowHost1); |
| const shadow1 = shadowHost1.attachShadow({mode: 'open'}); |
| shadow1.innerHTML = '<div>shadow base</div>'; |
| shadow1.innerHTML = '<div>shadow base</div><span>extended</span>'; |
| log('Set shadowRoot.innerHTML twice with prefix match'); |
| log('shadow children.length: ' + shadow1.children.length + ' (expected: 2)'); |
| log('Result: ' + (shadow1.children.length === 2 ? 'PASS' : 'FAIL') + '\n'); |
| shadow1.innerHTML = ''; |
| |
| log('=== Shadow DOM mutation invalidates cache ==='); |
| const shadowHost2 = document.createElement('div'); |
| document.body.appendChild(shadowHost2); |
| const shadow2 = shadowHost2.attachShadow({mode: 'open'}); |
| shadow2.innerHTML = '<div id="shadow-test">content</div>'; |
| shadow2.innerHTML = '<div id="shadow-test">content</div><span>extension</span>'; |
| shadow2.getElementById('shadow-test').appendChild(document.createElement('b')); |
| shadow2.innerHTML = '<div id="shadow-test">content</div><span>extension</span><p>more</p>'; |
| log('Mutate shadow content then extend'); |
| log('Result: ' + (!shadow2.getElementById('shadow-test').querySelector('b') ? 'PASS' : 'FAIL') + '\n'); |
| shadow2.innerHTML = ''; |
| } |
| |
| function testCrossContainer() { |
| log('---------- Cross-Container Tests ----------\n'); |
| |
| log('=== Cache can be reused across different containers ==='); |
| const container1 = document.createElement('div'); |
| const container2 = document.createElement('div'); |
| const container3 = document.createElement('div'); |
| document.body.appendChild(container1); |
| document.body.appendChild(container2); |
| document.body.appendChild(container3); |
| |
| // Set initial content in container1 (this caches it) |
| container1.innerHTML = '<div id="cross-container-test">content</div><span>more</span>'; |
| |
| // Set same content in container2 (should reuse cache from container1) |
| container2.innerHTML = '<div id="cross-container-test">content</div><span>more</span>'; |
| |
| // Extend in container3 with prefix match (should reuse cache) |
| container3.innerHTML = '<div id="cross-container-test">content</div><span>more</span><p>extended</p>'; |
| |
| log('Set same innerHTML across multiple containers'); |
| log('container1 has content: ' + (container1.querySelector('#cross-container-test') !== null)); |
| log('container2 has content: ' + (container2.querySelector('#cross-container-test') !== null)); |
| log('container3 has extended content: ' + (container3.querySelector('p') !== null)); |
| log('Result: ' + (container1.querySelector('#cross-container-test') && container2.querySelector('#cross-container-test') && container3.querySelector('p') ? 'PASS' : 'FAIL') + '\n'); |
| |
| container1.innerHTML = ''; |
| container2.innerHTML = ''; |
| container3.innerHTML = ''; |
| |
| log('=== Mutation in one container invalidates cache for all ==='); |
| const mutateContainer1 = document.createElement('div'); |
| const mutateContainer2 = document.createElement('div'); |
| document.body.appendChild(mutateContainer1); |
| document.body.appendChild(mutateContainer2); |
| |
| // Set content in first container (caches it) |
| mutateContainer1.innerHTML = '<div id="mutation-cross-test">original</div>'; |
| mutateContainer1.innerHTML = '<div id="mutation-cross-test">original</div><span>extended</span>'; |
| |
| // Mutate the cached DOM |
| mutateContainer1.querySelector('#mutation-cross-test').appendChild(document.createElement('b')); |
| |
| // Try to use cache in a different container |
| // Should create fresh nodes (cache was invalidated by mutation) |
| mutateContainer2.innerHTML = '<div id="mutation-cross-test">original</div><span>extended</span>'; |
| |
| log('Mutated container1, then set innerHTML in container2'); |
| log('container2 has no <b> element: ' + (!mutateContainer2.querySelector('#mutation-cross-test b'))); |
| log('Result: ' + (!mutateContainer2.querySelector('#mutation-cross-test b') ? 'PASS' : 'FAIL') + '\n'); |
| |
| mutateContainer1.innerHTML = ''; |
| mutateContainer2.innerHTML = ''; |
| } |
| |
| function runTest() { |
| testPrefixReuse(); |
| testDOMMutations(); |
| testAttributeMutations(); |
| testStringLength(); |
| testShadowDOM(); |
| testCrossContainer(); |
| } |
| </script> |
| </head> |
| <body onload="runTest()"> |
| <p>Tests innerHTML prefix cache mutations and invalidation.</p> |
| <pre id="console"></pre> |
| </body> |
| </html> |