attribute-changed-callback.html (13522B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>Custom Elements: attributeChangedCallback</title> 5 <meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> 6 <meta name="assert" content="attributeChangedCallback must be enqueued whenever custom element's attribute is added, changed or removed"> 7 <link rel="help" href="https://w3c.github.io/webcomponents/spec/custom/#dfn-attribute-changed-callback"> 8 <script src="/resources/testharness.js"></script> 9 <script src="/resources/testharnessreport.js"></script> 10 <script src="resources/custom-elements-helpers.js"></script> 11 </head> 12 <body> 13 <div id="log"></div> 14 <parser-created-element title></parser-created-element> 15 <script> 16 17 var customElement = define_new_custom_element(['title', 'id', 'r']); 18 19 test(function () { 20 const instance = document.createElement(customElement.name); 21 assert_array_equals(customElement.takeLog().types(), ['constructed']); 22 23 instance.setAttribute('title', 'foo'); 24 assert_equals(instance.getAttribute('title'), 'foo'); 25 var logEntries = customElement.takeLog(); 26 assert_array_equals(logEntries.types(), ['attributeChanged']); 27 assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: null, newValue: 'foo', namespace: null}); 28 29 instance.removeAttribute('title'); 30 assert_equals(instance.getAttribute('title'), null); 31 var logEntries = customElement.takeLog(); 32 assert_array_equals(logEntries.types(), ['attributeChanged']); 33 assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: 'foo', newValue: null, namespace: null}); 34 }, 'setAttribute and removeAttribute must enqueue and invoke attributeChangedCallback'); 35 36 test(function () { 37 var instance = document.createElement(customElement.name); 38 assert_array_equals(customElement.takeLog().types(), ['constructed']); 39 40 instance.setAttributeNS('http://www.w3.org/2000/svg', 'title', 'hello'); 41 assert_equals(instance.getAttribute('title'), 'hello'); 42 var logEntries = customElement.takeLog(); 43 assert_array_equals(logEntries.types(), ['attributeChanged']); 44 assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: null, newValue: 'hello', namespace: 'http://www.w3.org/2000/svg'}); 45 46 instance.removeAttributeNS('http://www.w3.org/2000/svg', 'title'); 47 assert_equals(instance.getAttribute('title'), null); 48 var logEntries = customElement.takeLog(); 49 assert_array_equals(logEntries.types(), ['attributeChanged']); 50 assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: 'hello', newValue: null, namespace: 'http://www.w3.org/2000/svg'}); 51 }, 'setAttributeNS and removeAttributeNS must enqueue and invoke attributeChangedCallback'); 52 53 test(function () { 54 var instance = document.createElement(customElement.name); 55 assert_array_equals(customElement.takeLog().types(), ['constructed']); 56 57 var attr = document.createAttribute('id'); 58 attr.value = 'bar'; 59 instance.setAttributeNode(attr); 60 61 assert_equals(instance.getAttribute('id'), 'bar'); 62 var logEntries = customElement.takeLog(); 63 assert_array_equals(logEntries.types(), ['attributeChanged']); 64 assert_attribute_log_entry(logEntries.last(), {name: 'id', oldValue: null, newValue: 'bar', namespace: null}); 65 66 instance.removeAttributeNode(attr); 67 assert_equals(instance.getAttribute('id'), null); 68 var logEntries = customElement.takeLog(); 69 assert_array_equals(logEntries.types(), ['attributeChanged']); 70 assert_attribute_log_entry(logEntries.last(), {name: 'id', oldValue: 'bar', newValue: null, namespace: null}); 71 }, 'setAttributeNode and removeAttributeNode must enqueue and invoke attributeChangedCallback for an HTML attribute'); 72 73 test(function () { 74 const instance = document.createElement(customElement.name); 75 assert_array_equals(customElement.takeLog().types(), ['constructed']); 76 77 const attr = document.createAttributeNS('http://www.w3.org/2000/svg', 'r'); 78 attr.value = '100'; 79 instance.setAttributeNode(attr); 80 81 assert_equals(instance.getAttribute('r'), '100'); 82 var logEntries = customElement.takeLog(); 83 assert_array_equals(logEntries.types(), ['attributeChanged']); 84 assert_attribute_log_entry(logEntries.last(), {name: 'r', oldValue: null, newValue: '100', namespace: 'http://www.w3.org/2000/svg'}); 85 86 instance.removeAttributeNode(attr); 87 assert_equals(instance.getAttribute('r'), null); 88 var logEntries = customElement.takeLog(); 89 assert_array_equals(logEntries.types(), ['attributeChanged']); 90 assert_attribute_log_entry(logEntries.last(), {name: 'r', oldValue: '100', newValue: null, namespace: 'http://www.w3.org/2000/svg'}); 91 }, 'setAttributeNode and removeAttributeNS must enqueue and invoke attributeChangedCallback for an SVG attribute'); 92 93 test(function () { 94 const instance = document.createElement(customElement.name); 95 assert_array_equals(customElement.takeLog().types(), ['constructed']); 96 97 instance.toggleAttribute('title', true); 98 assert_equals(instance.hasAttribute('title'), true); 99 var logEntries = customElement.takeLog(); 100 assert_array_equals(logEntries.types(), ['attributeChanged']); 101 assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: null, newValue: '', namespace: null}); 102 103 instance.toggleAttribute('title'); 104 assert_equals(instance.hasAttribute('title'), false); 105 var logEntries = customElement.takeLog(); 106 assert_array_equals(logEntries.types(), ['attributeChanged']); 107 assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: '', newValue: null, namespace: null}); 108 }, 'toggleAttribute must enqueue and invoke attributeChangedCallback'); 109 110 test(function () { 111 const callsToOld = []; 112 const callsToNew = []; 113 class CustomElement extends HTMLElement { } 114 CustomElement.prototype.attributeChangedCallback = function (...args) { 115 callsToOld.push(create_attribute_changed_callback_log(this, ...args)); 116 } 117 CustomElement.observedAttributes = ['title']; 118 customElements.define('element-with-mutated-attribute-changed-callback', CustomElement); 119 CustomElement.prototype.attributeChangedCallback = function (...args) { 120 callsToNew.push(create_attribute_changed_callback_log(this, ...args)); 121 } 122 123 const instance = document.createElement('element-with-mutated-attribute-changed-callback'); 124 instance.setAttribute('title', 'hi'); 125 assert_equals(instance.getAttribute('title'), 'hi'); 126 assert_array_equals(callsToNew, []); 127 assert_equals(callsToOld.length, 1); 128 assert_attribute_log_entry(callsToOld[0], {name: 'title', oldValue: null, newValue: 'hi', namespace: null}); 129 }, 'Mutating attributeChangedCallback after calling customElements.define must not affect the callback being invoked'); 130 131 test(function () { 132 const calls = []; 133 class CustomElement extends HTMLElement { 134 attributeChangedCallback(...args) { 135 calls.push(create_attribute_changed_callback_log(this, ...args)); 136 } 137 } 138 CustomElement.observedAttributes = ['title']; 139 customElements.define('element-not-observing-id-attribute', CustomElement); 140 141 const instance = document.createElement('element-not-observing-id-attribute'); 142 assert_equals(calls.length, 0); 143 instance.setAttribute('title', 'hi'); 144 assert_equals(calls.length, 1); 145 assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: 'hi', namespace: null}); 146 instance.setAttribute('id', 'some'); 147 assert_equals(calls.length, 1); 148 assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: 'hi', namespace: null}); 149 }, 'attributedChangedCallback must not be invoked when the observed attributes does not contain the attribute'); 150 151 test(function () { 152 const calls = []; 153 class CustomElement extends HTMLElement { } 154 CustomElement.prototype.attributeChangedCallback = function (...args) { 155 calls.push(create_attribute_changed_callback_log(this, ...args)); 156 } 157 CustomElement.observedAttributes = ['title', 'lang']; 158 customElements.define('element-with-mutated-observed-attributes', CustomElement); 159 CustomElement.observedAttributes = ['title', 'id']; 160 161 const instance = document.createElement('element-with-mutated-observed-attributes'); 162 instance.setAttribute('title', 'hi'); 163 assert_equals(calls.length, 1); 164 assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: 'hi', namespace: null}); 165 166 instance.setAttribute('id', 'some'); 167 assert_equals(calls.length, 1); 168 169 instance.setAttribute('lang', 'en'); 170 assert_equals(calls.length, 2); 171 assert_attribute_log_entry(calls[1], {name: 'lang', oldValue: null, newValue: 'en', namespace: null}); 172 }, 'Mutating observedAttributes after calling customElements.define must not affect the set of attributes for which attributedChangedCallback is invoked'); 173 174 test(function () { 175 var calls = []; 176 class CustomElement extends HTMLElement { } 177 CustomElement.prototype.attributeChangedCallback = function (...args) { 178 calls.push(create_attribute_changed_callback_log(this, ...args)); 179 } 180 CustomElement.observedAttributes = { [Symbol.iterator]: function *() { yield 'lang'; yield 'style'; } }; 181 customElements.define('element-with-generator-observed-attributes', CustomElement); 182 183 var instance = document.createElement('element-with-generator-observed-attributes'); 184 instance.setAttribute('lang', 'en'); 185 assert_equals(calls.length, 1); 186 assert_attribute_log_entry(calls[0], {name: 'lang', oldValue: null, newValue: 'en', namespace: null}); 187 188 instance.setAttribute('lang', 'ja'); 189 assert_equals(calls.length, 2); 190 assert_attribute_log_entry(calls[1], {name: 'lang', oldValue: 'en', newValue: 'ja', namespace: null}); 191 192 instance.setAttribute('title', 'hello'); 193 assert_equals(calls.length, 2); 194 195 instance.setAttribute('style', 'font-size: 2rem'); 196 assert_equals(calls.length, 3); 197 assert_attribute_log_entry(calls[2], {name: 'style', oldValue: null, newValue: 'font-size: 2rem', namespace: null}); 198 }, 'attributedChangedCallback must be enqueued for attributes specified in a non-Array iterable observedAttributes'); 199 200 test(function () { 201 var calls = []; 202 class CustomElement extends HTMLElement { } 203 CustomElement.prototype.attributeChangedCallback = function (...args) { 204 calls.push(create_attribute_changed_callback_log(this, ...args)); 205 } 206 CustomElement.observedAttributes = ['style']; 207 customElements.define('element-with-style-attribute-observation', CustomElement); 208 209 var instance = document.createElement('element-with-style-attribute-observation'); 210 assert_equals(calls.length, 0); 211 212 instance.style.fontSize = '10px'; 213 assert_equals(calls.length, 1); 214 assert_attribute_log_entry(calls[0], {name: 'style', oldValue: null, newValue: 'font-size: 10px;', namespace: null}); 215 216 instance.style.fontSize = '20px'; 217 assert_equals(calls.length, 2); 218 assert_attribute_log_entry(calls[1], {name: 'style', oldValue: 'font-size: 10px;', newValue: 'font-size: 20px;', namespace: null}); 219 220 }, 'attributedChangedCallback must be enqueued for style attribute change by mutating inline style declaration'); 221 222 test(function () { 223 var calls = []; 224 class CustomElement extends HTMLElement { } 225 CustomElement.prototype.attributeChangedCallback = function (...args) { 226 calls.push(create_attribute_changed_callback_log(this, ...args)); 227 } 228 CustomElement.observedAttributes = ['title']; 229 customElements.define('element-with-no-style-attribute-observation', CustomElement); 230 231 var instance = document.createElement('element-with-no-style-attribute-observation'); 232 assert_equals(calls.length, 0); 233 instance.style.fontSize = '10px'; 234 assert_equals(calls.length, 0); 235 instance.title = 'hello'; 236 assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: 'hello', namespace: null}); 237 }, 'attributedChangedCallback must not be enqueued when mutating inline style declaration if the style attribute is not observed'); 238 239 test(function () { 240 var calls = []; 241 class CustomElement extends HTMLElement { } 242 CustomElement.prototype.attributeChangedCallback = function (...args) { 243 calls.push(create_attribute_changed_callback_log(this, ...args)); 244 } 245 CustomElement.observedAttributes = ['title']; 246 customElements.define('parser-created-element', CustomElement); 247 assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: '', namespace: null}); 248 }, 'Upgrading a parser created element must enqueue and invoke attributeChangedCallback for an HTML attribute'); 249 250 test(function () { 251 var calls = []; 252 class CustomElement extends HTMLElement { } 253 CustomElement.prototype.attributeChangedCallback = function (...args) { 254 calls.push(create_attribute_changed_callback_log(this, ...args)); 255 } 256 CustomElement.observedAttributes = ['title']; 257 customElements.define('cloned-element-with-attribute', CustomElement); 258 259 var instance = document.createElement('cloned-element-with-attribute'); 260 assert_equals(calls.length, 0); 261 instance.title = ''; 262 assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: '', namespace: null}); 263 264 calls = []; 265 var clone = instance.cloneNode(false); 266 assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: '', namespace: null}); 267 }, 'Upgrading a cloned element must enqueue and invoke attributeChangedCallback for an HTML attribute'); 268 269 </script> 270 </body> 271 </html>