ElementInternals-validation.html (14241B)
1 <!DOCTYPE html> 2 <title>Form validation features of ElementInternals, and :valid :invalid pseudo classes</title> 3 <body> 4 <script src="/resources/testharness.js"></script> 5 <script src="/resources/testharnessreport.js"></script> 6 <div id="container"></div> 7 <script> 8 class MyControl extends HTMLElement { 9 static get formAssociated() { return true; } 10 11 constructor() { 12 super(); 13 this.internals_ = this.attachInternals(); 14 } 15 get i() { return this.internals_; } 16 } 17 customElements.define('my-control', MyControl); 18 19 class NotFormAssociatedElement extends HTMLElement { 20 constructor() { 21 super(); 22 this.internals_ = this.attachInternals(); 23 } 24 get i() { return this.internals_; } 25 } 26 customElements.define('not-form-associated-element', NotFormAssociatedElement); 27 28 test(() => { 29 const control = new MyControl(); 30 assert_true(control.i.willValidate, 'default value is true'); 31 32 const datalist = document.createElement('datalist'); 33 datalist.appendChild(control); 34 assert_false(control.i.willValidate, 'false in DATALIST'); 35 datalist.removeChild(control); 36 assert_true(control.i.willValidate, 'remove from DATALIST'); 37 38 const fieldset = document.createElement('fieldset'); 39 fieldset.disabled = true; 40 fieldset.appendChild(control); 41 assert_false(control.i.willValidate, 'append to disabled FIELDSET'); 42 fieldset.disabled = false; 43 assert_true(control.i.willValidate, 'FIELDSET becomes enabled'); 44 fieldset.disabled = true; 45 assert_false(control.i.willValidate, 'FIELDSET becomes disabled'); 46 fieldset.removeChild(control); 47 assert_true(control.i.willValidate, 'remove from disabled FIELDSET'); 48 49 control.setAttribute('disabled', ''); 50 assert_false(control.i.willValidate, 'with disabled attribute'); 51 control.removeAttribute('disabled'); 52 assert_true(control.i.willValidate, 'without disabled attribute'); 53 54 control.setAttribute('readonly', ''); 55 assert_false(control.i.willValidate, 'with readonly attribute'); 56 control.removeAttribute('readonly'); 57 assert_true(control.i.willValidate, 'without readonly attribute'); 58 }, 'willValidate'); 59 60 test(() => { 61 const container = document.getElementById("container"); 62 container.innerHTML = '<will-be-defined></will-be-defined>' + 63 '<will-be-defined disabled></will-be-defined>' + 64 '<will-be-defined readonly></will-be-defined>' + 65 '<datalist><will-be-defined></will-be-defined></datalist>' + 66 '<fieldset disabled><will-be-defined></will-be-defined></fieldset>'; 67 68 class WillBeDefined extends HTMLElement { 69 static get formAssociated() { return true; } 70 71 constructor() { 72 super(); 73 this.internals_ = this.attachInternals(); 74 } 75 get i() { return this.internals_; } 76 } 77 customElements.define('will-be-defined', WillBeDefined); 78 customElements.upgrade(container); 79 80 const controls = document.querySelectorAll('will-be-defined'); 81 assert_true(controls[0].i.willValidate, 'default value'); 82 assert_false(controls[1].i.willValidate, 'with disabled attribute'); 83 assert_false(controls[2].i.willValidate, 'with readOnly attribute'); 84 assert_false(controls[3].i.willValidate, 'in datalist'); 85 assert_false(controls[4].i.willValidate, 'in disabled fieldset'); 86 }, 'willValidate after upgrade'); 87 88 test(t => { 89 const control = document.createElement('will-be-defined-2'); 90 91 customElements.define('will-be-defined-2', class extends HTMLElement { 92 static get formAssociated() { return true; } 93 }); 94 95 container.append(control); 96 t.add_cleanup(() => { container.innerHTML = ''; }); 97 98 const i = control.attachInternals(); 99 assert_true(i.willValidate); 100 }, 'willValidate after upgrade (document.createElement)'); 101 102 test(() => { 103 const element = new NotFormAssociatedElement(); 104 assert_throws_dom('NotSupportedError', () => element.i.willValidate); 105 }, "willValidate should throw NotSupportedError if the target element is not a form-associated custom element"); 106 107 test(() => { 108 const element = new NotFormAssociatedElement(); 109 assert_throws_dom('NotSupportedError', () => element.i.validity); 110 }, "validity should throw NotSupportedError if the target element is not a form-associated custom element"); 111 112 test(() => { 113 const element = new NotFormAssociatedElement(); 114 assert_throws_dom('NotSupportedError', () => element.i.validationMessage); 115 }, "validationMessage should throw NotSupportedError if the target element is not a form-associated custom element"); 116 117 test(() => { 118 const element = new NotFormAssociatedElement(); 119 assert_throws_dom('NotSupportedError', () => element.i.setValidity()); 120 }, "setValidity() should throw NotSupportedError if the target element is not a form-associated custom element"); 121 122 test(() => { 123 const control = document.createElement('my-control'); 124 const validity = control.i.validity; 125 assert_false(validity.valueMissing, 'default valueMissing'); 126 assert_false(validity.typeMismatch, 'default typeMismatch'); 127 assert_false(validity.patternMismatch, 'default patternMismatch'); 128 assert_false(validity.tooLong, 'default tooLong'); 129 assert_false(validity.tooShort, 'default tooShort'); 130 assert_false(validity.rangeUnderflow, 'default rangeUnderflow'); 131 assert_false(validity.rangeOverflow, 'default rangeOverflow'); 132 assert_false(validity.stepMismatch, 'default stepMismatch'); 133 assert_false(validity.badInput, 'default badInput'); 134 assert_false(validity.customError, 'default customError'); 135 assert_true(validity.valid, 'default valid'); 136 137 control.i.setValidity({valueMissing: true}, 'valueMissing message'); 138 assert_true(validity.valueMissing); 139 assert_false(validity.valid); 140 assert_equals(control.i.validationMessage, 'valueMissing message'); 141 142 control.i.setValidity({typeMismatch: true}, 'typeMismatch message'); 143 assert_true(validity.typeMismatch); 144 assert_false(validity.valueMissing); 145 assert_false(validity.valid); 146 assert_equals(control.i.validationMessage, 'typeMismatch message'); 147 148 control.i.setValidity({patternMismatch: true}, 'patternMismatch message'); 149 assert_true(validity.patternMismatch); 150 assert_false(validity.valid); 151 assert_equals(control.i.validationMessage, 'patternMismatch message'); 152 153 control.i.setValidity({tooLong: true}, 'tooLong message'); 154 assert_true(validity.tooLong); 155 assert_false(validity.valid); 156 assert_equals(control.i.validationMessage, 'tooLong message'); 157 158 control.i.setValidity({tooShort: true}, 'tooShort message'); 159 assert_true(validity.tooShort); 160 assert_false(validity.valid); 161 assert_equals(control.i.validationMessage, 'tooShort message'); 162 163 control.i.setValidity({rangeUnderflow: true}, 'rangeUnderflow message'); 164 assert_true(validity.rangeUnderflow); 165 assert_false(validity.valid); 166 assert_equals(control.i.validationMessage, 'rangeUnderflow message'); 167 168 control.i.setValidity({rangeOverflow: true}, 'rangeOverflow message'); 169 assert_true(validity.rangeOverflow); 170 assert_false(validity.valid); 171 assert_equals(control.i.validationMessage, 'rangeOverflow message'); 172 173 control.i.setValidity({stepMismatch: true}, 'stepMismatch message'); 174 assert_true(validity.stepMismatch); 175 assert_false(validity.valid); 176 assert_equals(control.i.validationMessage, 'stepMismatch message'); 177 178 control.i.setValidity({badInput: true}, 'badInput message'); 179 assert_true(validity.badInput); 180 assert_false(validity.valid); 181 assert_equals(control.i.validationMessage, 'badInput message'); 182 183 control.i.setValidity({customError: true}, 'customError message'); 184 assert_true(validity.customError, 'customError should be true.'); 185 assert_false(validity.valid); 186 assert_equals(control.i.validationMessage, 'customError message'); 187 188 // Set multiple flags 189 control.i.setValidity({badInput: true, customError: true}, 'multiple errors'); 190 assert_true(validity.badInput); 191 assert_true(validity.customError); 192 assert_false(validity.valid); 193 assert_equals(control.i.validationMessage, 'multiple errors'); 194 195 // Clear flags 196 control.i.setValidity({}, 'unnecessary message'); 197 assert_false(validity.badInput); 198 assert_false(validity.customError); 199 assert_true(validity.valid); 200 assert_equals(control.i.validationMessage, ''); 201 202 assert_throws_js(TypeError, () => { control.i.setValidity({valueMissing: true}); }, 203 'setValidity() requires the second argument if the first argument contains true'); 204 }, 'validity and setValidity()'); 205 206 test(() => { 207 const control = document.createElement('my-control'); 208 control.i.setValidity({badInput: true}, 'error message'); 209 const validity = control.i.validity; 210 assert_false(validity.customError); 211 assert_false(validity.valid); 212 assert_equals(control.i.validationMessage, 'error message'); 213 }, "validity.customError should be false if not explicitly set via setValidity()"); 214 215 test(() => { 216 document.body.insertAdjacentHTML('afterbegin', '<my-control><light-child></my-control>'); 217 let control = document.body.firstChild; 218 const flags = {valueMissing: true}; 219 const m = 'non-empty message'; 220 221 assert_throws_dom('NotFoundError', () => { 222 control.i.setValidity(flags, m, document.body); 223 }, 'Not a descendant'); 224 225 let notHTMLElement = document.createElementNS('some-random-namespace', 'foo'); 226 control.appendChild(notHTMLElement); 227 assert_throws_js(TypeError, () => { 228 control.i.setValidity(flags, m, notHTMLElement); 229 }, 'Not a HTMLElement'); 230 notHTMLElement.remove(); 231 232 // A descendant 233 control.i.setValidity(flags, m, control.querySelector('light-child')); 234 235 // An element in the direct shadow tree 236 let shadow1 = control.attachShadow({mode: 'open'}); 237 let shadowChild = document.createElement('div'); 238 shadow1.appendChild(shadowChild); 239 control.i.setValidity(flags, m, shadowChild); 240 241 // An element in an nested shadow trees 242 let shadow2 = shadowChild.attachShadow({mode: 'closed'}); 243 let nestedShadowChild = document.createElement('span'); 244 shadow2.appendChild(nestedShadowChild); 245 control.i.setValidity(flags, m, nestedShadowChild); 246 }, '"anchor" argument of setValidity()'); 247 248 test(t => { 249 const control = document.createElement('my-control'); 250 document.body.appendChild(control); 251 t.add_cleanup(() => control.remove()); 252 const flags = {valueMissing: true}; 253 const m = 'non-empty message'; 254 255 // Self 256 control.i.setValidity(flags, m, control); 257 }, '"anchor" argument of setValidity(): passing self'); 258 259 test(() => { 260 const element = new NotFormAssociatedElement(); 261 assert_throws_dom('NotSupportedError', () => element.i.checkValidity()); 262 }, "checkValidity() should throw NotSupportedError if the target element is not a form-associated custom element"); 263 264 test(() => { 265 const control = document.createElement('my-control'); 266 let invalidCount = 0; 267 control.addEventListener('invalid', e => { 268 assert_equals(e.target, control); 269 assert_true(e.cancelable); 270 ++invalidCount; 271 }); 272 273 assert_true(control.i.checkValidity(), 'default state'); 274 assert_equals(invalidCount, 0); 275 276 control.i.setValidity({customError:true}, 'foo'); 277 assert_false(control.i.checkValidity()); 278 assert_equals(invalidCount, 1); 279 }, 'checkValidity()'); 280 281 test(() => { 282 const element = new NotFormAssociatedElement(); 283 assert_throws_dom('NotSupportedError', () => element.i.reportValidity()); 284 }, "reportValidity() should throw NotSupportedError if the target element is not a form-associated custom element"); 285 286 test(() => { 287 const control = document.createElement('my-control'); 288 document.body.appendChild(control); 289 control.tabIndex = 0; 290 let invalidCount = 0; 291 control.addEventListener('invalid', e => { 292 assert_equals(e.target, control); 293 assert_true(e.cancelable); 294 ++invalidCount; 295 }); 296 297 assert_true(control.i.reportValidity(), 'default state'); 298 assert_equals(invalidCount, 0); 299 300 control.i.setValidity({customError:true}, 'foo'); 301 assert_false(control.i.reportValidity()); 302 assert_equals(invalidCount, 1); 303 304 control.blur(); 305 control.addEventListener('invalid', e => e.preventDefault()); 306 assert_false(control.i.reportValidity()); 307 }, 'reportValidity()'); 308 309 test(() => { 310 const container = document.getElementById('container'); 311 container.innerHTML = '<form><input type=submit><my-control>'; 312 const form = container.querySelector('form'); 313 const control = container.querySelector('my-control'); 314 control.tabIndex = 0; 315 let invalidCount = 0; 316 control.addEventListener('invalid', e => { ++invalidCount; }); 317 318 assert_true(control.i.checkValidity()); 319 assert_true(form.checkValidity()); 320 control.i.setValidity({valueMissing: true}, 'Please fill out this field'); 321 assert_false(form.checkValidity()); 322 assert_equals(invalidCount, 1); 323 324 assert_false(form.reportValidity()); 325 assert_equals(invalidCount, 2); 326 327 container.querySelector('input[type=submit]').click(); 328 assert_equals(invalidCount, 3); 329 }, 'Custom control affects validation at the owner form'); 330 331 function isValidAccordingToCSS(element, comment) { 332 assert_true(element.matches(':valid'), comment ? (comment + ' - :valid') : undefined); 333 assert_false(element.matches(':invalid'), comment ? (comment + ' - :invalid') : undefined); 334 } 335 336 function isInvalidAccordingToCSS(element, comment) { 337 assert_false(element.matches(':valid'), comment ? (comment + ' - :valid') : undefined); 338 assert_true(element.matches(':invalid'), comment ? (comment + ' - :invalid') : undefined); 339 } 340 341 test(() => { 342 const container = document.getElementById('container'); 343 container.innerHTML = '<form><fieldset><my-control>'; 344 const form = container.querySelector('form'); 345 const fieldset = container.querySelector('fieldset'); 346 const control = container.querySelector('my-control'); 347 348 isValidAccordingToCSS(control); 349 isValidAccordingToCSS(form); 350 isValidAccordingToCSS(fieldset); 351 352 control.i.setValidity({typeMismatch: true}, 'Invalid format'); 353 isInvalidAccordingToCSS(control); 354 isInvalidAccordingToCSS(form); 355 isInvalidAccordingToCSS(fieldset); 356 357 control.remove(); 358 isInvalidAccordingToCSS(control); 359 isValidAccordingToCSS(form); 360 isValidAccordingToCSS(fieldset); 361 362 fieldset.appendChild(control); 363 isInvalidAccordingToCSS(form); 364 isInvalidAccordingToCSS(fieldset); 365 366 control.i.setValidity({}); 367 isValidAccordingToCSS(control); 368 isValidAccordingToCSS(form); 369 isValidAccordingToCSS(fieldset); 370 }, 'Custom control affects :valid :invalid for FORM and FIELDSET'); 371 </script> 372 </body>